Release
Shipping the mobile app - build profiles, versioning, when an update goes over the air, and what the release workflow needs.
Releases run through EAS Build and the Mobile release workflow, which is dispatch-only in a generated project: nothing ships because a commit landed.
Build profiles
apps/mobile/eas.json ships four:
| Profile | Distribution | Channel | For |
|---|---|---|---|
development | internal, dev client | development | Day-to-day work. Its iOS artifact sets ios.simulator, so it runs on a simulator |
development-device | internal, dev client | development | The same dev client on a real phone - no ios.simulator, so the artifact installs |
preview | internal | preview | Sharing a build with testers |
production | store | production | The build you submit |
Both development profiles share the development channel, so one published update reaches a simulator and a phone alike. A development build is development-signed: iOS 16 and later asks for Developer Mode on the phone (Settings -> Privacy & Security), which an ad-hoc preview build does not.
A profile's FIRST build has to run interactively. eas build --non-interactive cannot create a signing certificate or a provisioning profile, so run the profile once without that flag (or eas credentials) and let EAS make them. If credentials:configure-build then fails while syncing Apple capabilities - a known eas-cli 22.6 defect, reported by a project generated from this repo - set EXPO_NO_CAPABILITY_SYNC=1 and enable the capabilities in App Store Connect instead.
Versioning
The version is computed the way every other release here is: a feat since the last tag bumps the minor, otherwise the patch, and a major needs a deliberate -f bump=major dispatch.
| Fact | Behaviour |
|---|---|
| Tag line | mobile-v*, its own; it never moves with the web release |
| Where the number goes | apps/mobile/package.json in the runner's tree, never committed back |
| What it gates on | A diff over the mobile app, the shared styles, and the @repo/* packages the app depends on |
| Build numbers | EAS owns them: appVersionSource is remote and production auto-increments |
Over the air, or a new build
| Case | What ships |
|---|---|
| The fingerprint matches that platform's newest finished production build | An OTA update - no store review, no new binary |
| It differs, or there is no prior build | A full EAS build for that platform |
apps/mobile/fingerprint.config.js is gone | A full build every time |
That file's sourceSkips keeps the version fields the workflow rewrites out of the hash, so a version-only release still matches. It REPLACES the defaults, so keep both skip names.
What the workflow needs
| Value | Kind | Effect when missing |
|---|---|---|
EXPO_TOKEN | Repository secret | The workflow skips green, naming it; a repo without EAS credentials is never a red release |
EAS_PROJECT_ID | Repository variable | The same green skip; app.config.ts reads it |
MOBILE_ANDROID_SUBMIT_READY | Repository variable | Android builds but is not auto-submitted |
EXPO_TOKEN is the EAS robot token, not EXPO_ACCESS_TOKEN, which is the backend's push bearer. Upload the APNs key and FCM V1 credentials once with pnpm --filter mobile exec eas credentials.
Submitting
The first submission on each store is manual: the listing must exist first, and no automated path creates it - App Store Connect's API offers no create for apps, so that record is made once in the web UI. Everything after it can run unattended. From the second release a configured submit profile adds --auto-submit, and Android's release status is draft, so a submitted build waits for you to roll it out.
Set MOBILE_ANDROID_SUBMIT_READY to true once eas credentials shows a Google Service Account key on the production profile. That key lives in EAS, not in eas.json, so nothing in the repo can detect it.
An UNATTENDED iOS submission needs four fields on the submit profile, not one: ascAppId plus ascApiKeyPath, ascApiKeyId and ascApiKeyIssuerId. Without ascAppId, eas submit --non-interactive cannot run at all; the three key fields are what let it authenticate from a local App Store Connect API key instead of prompting. This repo ships none of them - the first submission is manual by design, and an empty string in any of them fails eas build too. Apple's own xcrun altool --upload-package <ipa> --api-key <key-id> --api-issuer <issuer-id> is the alternative when you would rather not put a key path in the repo.
Once a store approves, paste its listing URL into packages/config/src/mobile.mjs. The "get the app" section stays hidden until one is set, and a store without a URL drops only its own badge.
| Field | Paste |
|---|---|
config.mobile.stores.ios | App Store listing URL |
config.mobile.stores.android | Google Play listing URL |