Mobile apps on a deployed stage
A MobileApp is built by EAS and shipped by the stores, so nothing in a deploy runs it. What it is built with, though, is the stage's: the URLs of the API and auth server it calls, and its URL scheme. gkm derives those from the workspace the same way it does for a site, so a store build talks to the same servers the stage was deployed with.
// constructs/mobile.ts
import { MobileApp } from '@geekmidas/constructs/mobile-app';
export const app = new MobileApp('App', { path: 'apps/app' }).dependsOn([
api,
auth,
]);Locally, gkm dev and gkm exec hand the app its values. A store build runs on EAS's machines, outside gkm, so a deployed stage's values have to reach EAS.
What a stage's build reads
For a deployed compose stage, the app's build reads:
- its scheme, under the key its
app.config.tsreads (APP_SCHEME); - each edge's public value under
EXPO_PUBLIC_:EXPO_PUBLIC_API_URL,EXPO_PUBLIC_AUTH_URL, as the stage's public addresses; - every stage secret already named
EXPO_PUBLIC_…, such asEXPO_PUBLIC_STORE_URLset withgkm secrets:set.
Nothing else. A server key never reaches a mobile build, and a deployed stage has no Mailpit inbox.
How each stage ships
Each deployed stage reads an EAS environment, listens on an EAS Update channel, and submits Android builds to a Play track:
| Stage | EAS environment | Channel | Play track | Release status |
|---|---|---|---|---|
the protected one (stages.protected) | production | the stage's name | production | draft |
| every other deployed stage | preview | the stage's name | internal | draft |
A draft release waits in Play Console until someone releases it. A stage that ships differently says so on the MobileApp:
new MobileApp('App', {
path: 'apps/app',
stages: {
beta: { android: { track: 'alpha', releaseStatus: 'completed' } },
},
});A stage named there that stages.deployed doesn't list is refused (MobileAppStageUndeclared). EAS's free plan only has the production, preview and development environments.
gkm eas:sync
gkm eas:sync --stage prod --profile acme-prodRun from the app's directory, or anywhere in a workspace with one mobile app (--app <app> picks one of several). It:
- writes the stage's profiles into the app's
eas.json:build.<stage>gets itsenvironmentandchannel, andsubmit.<stage>.androiditstrackandreleaseStatus. Every other field is kept, soautoIncrement,corepackor an iOSascAppIdstay as written; - sets each value the build reads in the stage's EAS environment, with
eas env:set, asplaintext: a bundle inlines them, so none is secret.
--dry-run prints the profiles and values and writes nothing. It needs the app's eas, installed in the app (pnpm add -D eas-cli) or globally, and an EAS login: eas login here, EXPO_TOKEN in CI. The stage's secrets are read the way a deploy reads them; --profile names the AWS profile for an S3, SSM or Secrets Manager store.
Then build with the stage's profile:
eas build --profile prod --platform allgkm publish
gkm publish --stage prodShips the app's current code to the stage's users, the cheapest way that gets it there. It syncs first (as gkm eas:sync), then, for iOS and Android:
- computes the fingerprint of the app's native code with the stage's build profile (
eas fingerprint:generate). WithruntimeVersion: { policy: 'fingerprint' }inapp.config.ts, that is the runtime version a build gets; - looks for a finished store build of the stage's profile with that fingerprint (
eas build:list); - if there is one, only JavaScript changed: it publishes an update on the stage's channel (
eas update --channel <stage>), which phones running that build download. The message is the commit's subject unless--messagenames one; - if there is none, it starts a store build (
eas build --profile <stage> --auto-submit --no-wait), submitted as the stage's submit profile says: Android to its Play track, iOS to App Store Connect oncesubmit.<stage>.ios.ascAppIdis set ineas.json. Without it, iOS is built and not submitted, and gkm says so.
| Option | |
|---|---|
--platform ios|android|all | One platform, or both (the default) |
--build | A store build, whatever the fingerprint says |
--update | An update, whatever the fingerprint says. Refused when no store build has the native code (NoStoreBuildForUpdate): it would reach nobody |
--wait | Wait for the build, and its submission, on EAS |
--dry-run | Say what it would release, and release nothing. It still asks EAS for the fingerprint and the builds |
Run by EAS: the stage's workflow
gkm eas:sync also writes the stage's EAS workflow, .eas/workflows/publish-<stage>.yml in the app, which does what gkm publish does using EAS's own jobs. Shipping the app then needs nothing in the project's CI: no runner, no eas-cli install, and no token in GitHub.
fingerprintreads the native code's fingerprint with the stage's EAS environment.get-build, per platform, looks for a finished store build of the stage's profile with that fingerprint.- If there is one:
updatepublishes on the stage's channel. - If there is none:
buildmakes a store build, andsubmitsends it with the stage's submit profile. iOS gets asubmitjob oncesubmit.<stage>.ios.ascAppIdis ineas.json.
It runs when the stage is deployed: the protected stage on a release tag (v*), every other deployed stage on a push to main. Any of them can also be run by hand:
eas workflow:run .eas/workflows/publish-prod.ymlIt needs EAS's GitHub app connected to the repository (expo.dev → the project → GitHub). The values a build reads come from the stage's EAS environment, so run gkm eas:sync again when they change, for example after a new domain or a new dependency. The file is generated: change the stage on the MobileApp, not the YAML. gkm init writes one for each deployed stage.
Before the first store build
Each store needs things only an account owner can set up:
- iOS: the app in App Store Connect, its Apple ID in
submit.<stage>.ios.ascAppId, App Store distribution credentials (eas credentials -p ios), and an App Store Connect API key in EAS so CI can submit without an Apple login. - Android: the app in Play Console, and a Google service account key in EAS for EAS Submit:
gkm deploy:google-playsets up the key (below). Play requires the very first upload of a new app to be done by hand.
gkm deploy:google-play
gkm deploy:google-play --developer 1234567890Sets up what EAS Submit needs to publish to Google Play, the way gkm deploy:github sets up the AWS role CI deploys with. Run it as a Play Console admin. Running it again rotates the key and pushes the listing.
eas.json: each deployed stage'ssubmit.<stage>.androidtrack and release status.- Google: the project (
--project, else<workspace>-play, created when absent) with the Android Publisher API on, and the service account<workspace>-playin it. The sign-in is gcloud's (gcloud auth application-default login), kept in gkm's own gcloud config (~/.gkm/gcloud), so your own gcloud setup is untouched. - Play Console: the service account invited with a grant on this app alone, and no account-wide permission. It can release to the testing tracks (
CAN_MANAGE_TRACK_APKS), to production only when a stage submits there (CAN_MANAGE_PUBLIC_APKS), and edit the listing when the app keeps one (CAN_MANAGE_PUBLIC_LISTING). A later run that needs different permissions updates the grant. - The key: a new JSON key, held in memory only and never written or printed. It's uploaded to EAS and assigned to the app's Android credentials for EAS Submit, then checked by opening and discarding an edit on the app with it. Only then are the account's older keys deleted from EAS and Google, so a failed run leaves the old key working.
- The listing, when the app keeps one, pushed through an edit.
- A checklist of what Play Console only takes by hand: privacy policy, app access, ads, content rating, target audience, health and sensitive permissions declarations.
Play knows the app by its package, which gkm reads from the app's Expo config (android.package), with its EAS project (extra.eas.projectId). It needs EXPO_TOKEN for EAS, and --developer: the number in Play Console's address.
Play has no API that creates an app. When the package isn't in Play Console, the run prints the link to create it. With someone at the terminal it then waits until the app exists; in CI it stops (PlayAppMissing) before any key is made.
The listing as code
The store listing lives beside the app, in fastlane supply's layout:
apps/app/store/play/
data-safety.csv # Play Console's data safety export
listings/en-US/
title.txt
short_description.txt
full_description.txt
video.txt # optional
images/
icon.png
featureGraphic.png
phoneScreenshots/1.png, 2.png …Each run replaces every image kind it finds, in file-name order. A new app that has never had an upload may refuse an edit; the listing is then skipped with a note, and the next run pushes it.
gkm exec --stage for a mobile app
From the app's directory, gkm exec --stage prod -- <command> runs the command with the same values eas:sync writes, which is enough for a local store build:
cd apps/app
gkm exec --stage prod -- eas build --profile prod --localIt needs no SSH: nothing a mobile app reads is on the stage's server network.