Skip to content

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.

ts
// 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.ts reads (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 as EXPO_PUBLIC_STORE_URL set with gkm 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:

StageEAS environmentChannelPlay trackRelease status
the protected one (stages.protected)productionthe stage's nameproductiondraft
every other deployed stagepreviewthe stage's nameinternaldraft

A draft release waits in Play Console until someone releases it. A stage that ships differently says so on the MobileApp:

ts
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 ​

bash
gkm eas:sync --stage prod --profile acme-prod

Run from the app's directory, or anywhere in a workspace with one mobile app (--app <app> picks one of several). It:

  1. writes the stage's profiles into the app's eas.json: build.<stage> gets its environment and channel, and submit.<stage>.android its track and releaseStatus. Every other field is kept, so autoIncrement, corepack or an iOS ascAppId stay as written;
  2. sets each value the build reads in the stage's EAS environment, with eas env:set, as plaintext: 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:

bash
eas build --profile prod --platform all

gkm publish ​

bash
gkm publish --stage prod

Ships 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:

  1. computes the fingerprint of the app's native code with the stage's build profile (eas fingerprint:generate). With runtimeVersion: { policy: 'fingerprint' } in app.config.ts, that is the runtime version a build gets;
  2. looks for a finished store build of the stage's profile with that fingerprint (eas build:list);
  3. 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 --message names one;
  4. 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 once submit.<stage>.ios.ascAppId is set in eas.json. Without it, iOS is built and not submitted, and gkm says so.
Option
--platform ios|android|allOne platform, or both (the default)
--buildA store build, whatever the fingerprint says
--updateAn update, whatever the fingerprint says. Refused when no store build has the native code (NoStoreBuildForUpdate): it would reach nobody
--waitWait for the build, and its submission, on EAS
--dry-runSay 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.

  • fingerprint reads 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: update publishes on the stage's channel.
  • If there is none: build makes a store build, and submit sends it with the stage's submit profile. iOS gets a submit job once submit.<stage>.ios.ascAppId is in eas.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:

bash
eas workflow:run .eas/workflows/publish-prod.yml

It 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-play sets up the key (below). Play requires the very first upload of a new app to be done by hand.

gkm deploy:google-play ​

bash
gkm deploy:google-play --developer 1234567890

Sets 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.

  1. eas.json: each deployed stage's submit.<stage>.android track and release status.
  2. Google: the project (--project, else <workspace>-play, created when absent) with the Android Publisher API on, and the service account <workspace>-play in 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.
  3. 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.
  4. 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.
  5. The listing, when the app keeps one, pushed through an edit.
  6. 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:

bash
cd apps/app
gkm exec --stage prod -- eas build --profile prod --local

It needs no SSH: nothing a mobile app reads is on the stage's server network.