Get packages/agents-mobile building reliably in CI and publishing through Expo/EAS with a workflow that mirrors packages/agents-desktop:
- PR builds for review and smoke testing.
- Canary builds from
main. - Stable release builds triggered by the existing changesets release workflow.
The mobile app is an Expo app. We should lean on Expo tooling for native builds, signing, submission, and preview distribution. Expo Go remains useful for local development where it works, but CI should prove the production build path because the app uses Expo DOM Components.
@electric-ax/agents-mobile is already a workspace package and Expo SDK 54 app. It uses Expo Router, React Native, and Expo DOM Components to embed selected agents-server-ui surfaces.
Draft PR: #4408
Implemented so far:
- Android PR preview builds via EAS internal distribution.
- Android canary builds from
mainvia EAS internal distribution. - Android stable release builds from Changesets via EAS production profile and Google Play Submit.
- Manual iOS simulator builds via EAS, before Apple Developer signing is available.
- Expo project, GitHub
EXPO_TOKEN, Google Play app, andGOOGLE_PLAY_SERVICE_ACCOUNT_JSONare configured.
Useful checks from the current app:
pnpm --filter @electric-ax/agents-mobile run ci:checkpasses when run with the repo's configured Node version.pnpm --filter @electric-ax/agents-mobile run export:iossucceeds.expo-doctorpasses 18/18 checks.expo config --type publicresolves:- owner:
electric-ax - slug:
agents-mobile - version/runtimeVersion: from
packages/agents-mobile/package.json - project id:
11a024df-c681-4374-867a-5c5905be9133 - Android package / iOS bundle id:
com.electricsql.agents.mobile
- owner:
Implemented package/config changes:
packages/agents-mobile/app.config.tsnow owns release metadata; the stale staticapp.jsonhas been removed.packages/agents-mobile/eas.jsonnow defines development, preview, preview-ios-simulator, canary, canary-store, and production profiles.- React, WebView, and TypeScript versions have been aligned across the mobile/server-ui/runtime graph so Expo doctor and mobile typecheck pass.
- Authenticated EAS builds will not run on untrusted fork PRs because GitHub does not expose repository secrets to those jobs. Forks should still get local checks; EAS builds should run for same-repository PRs, trusted labeled PRs, or manual dispatch.
Expo Go is good for fast local iteration and should remain documented as a developer path.
However, the production CI signal should come from EAS Build, not only Expo Go or EAS Update previews:
- The app uses Expo DOM Components via
'use dom'. - DOM Components support Expo Go, but Expo documents that DOM Components are embedded exports and do not currently support OTA updates in the normal EAS Update sense.
- DOM Components have enough production-build-specific behavior that preview and release binaries need to be built and installed as native apps.
Recommendation:
- Use Expo Go for local/manual development where possible.
- Use EAS internal distribution builds for PR validation where repository secrets are available.
- Use EAS internal distribution for canary validation until we deliberately enable Google Play internal-track submission.
- Use EAS Submit for Google Play and, later, App Store Connect/TestFlight.
Purpose: prove that the mobile app builds and produce an installable review artifact.
Initial scope:
- Android only.
- EAS internal distribution build for same-repository or otherwise trusted PRs.
- Local CI checks for all PRs, including fork PRs.
- No app-store submission.
- GitHub PR comment with the EAS build URL.
- Run only when mobile-impacting files change.
Later:
- Add optional/manual iOS simulator builds before Apple Developer setup.
- Add signed iOS internal builds after Apple Developer setup.
- Optionally add EAS Update preview comments for Expo Go/dev-build convenience, but not as the main CI artifact.
Purpose: continuously publish the latest main mobile build to internal testers.
Initial scope:
- Android build from
main. - Publish an EAS internal distribution build.
- Use an EAS
canaryprofile. - Trigger only when mobile-impacting files change.
Later:
- Optionally add a second
canary-storesubmission job to publish to the Google Play internal track. - Add iOS TestFlight once the Apple Developer account is available.
Purpose: publish store-ready mobile builds when changesets releases @electric-ax/agents-mobile.
Initial scope:
- Use the existing changesets release workflow as the source of truth.
- Capture the published
@electric-ax/agents-mobileversion and tag. - Build Android with a production EAS profile.
- Submit to Google Play using EAS Submit.
Later:
- Add iOS App Store/TestFlight submission from the same release trigger.
Purpose: prove that the app can compile as a native iOS app before Apple Developer signing is available.
Initial scope:
- Use an EAS
preview-ios-simulatorprofile withios.simulator: true. - Trigger manually via
agents_mobile_ios_simulator.yml. - Run the same mobile dependency build, typecheck, Expo doctor, Android export, and iOS export checks before starting the EAS build.
- Do not require Apple Developer credentials, provisioning profiles, App Store Connect, or TestFlight.
Limitations:
- Simulator artifacts are for local simulator testing only; they cannot be installed on physical devices.
- This does not validate App Store signing, entitlements, TestFlight submission, or App Review metadata.
- Signed device/TestFlight builds remain blocked on Apple Developer account access.
- Expo project is linked:
- owner:
electric-ax - slug:
agents-mobile - project id:
11a024df-c681-4374-867a-5c5905be9133
- owner:
EXPO_TOKENGitHub Actions repository secret is configured using an Expo Developer robot token.- Configure EAS Build credentials for Android signing.
We have a Google Play account, so Android can be the first publishing target.
Needed:
- Done: final Android package id is
com.electricsql.agents.mobile. - Done: Google Play app is created.
- Done: Google Play service account has been created and invited to the Play Console app.
- Done: Google Play service account JSON is stored as the GitHub Actions repository secret
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON. - Still possible: initial manual upload may be required if Google blocks API-based submission until the first AAB is uploaded through Play Console.
- Track decision:
- Canary: EAS internal distribution first; Google Play internal track is possible via
canary-storebut not yet enabled in CI. - Stable: production track, or staged rollout if preferred.
- Google Play app/store setup questionnaires may still need finishing before production release.
- Canary: EAS internal distribution first; Google Play internal track is possible via
Apple publishing should remain planned but disabled until the account exists. iOS simulator builds can run before that because they do not require signing.
Needed later:
- Apple Developer account.
- Bundle id, matching Android:
com.electricsql.agents.mobile. - App Store Connect app.
- EAS-managed iOS credentials.
- Signed iOS internal distribution profile.
- TestFlight submit profile.
packages/agents-mobile/app.config.ts owns build metadata derived from CI environment variables and package.json.
The config includes:
name:Electric Agentsslug:agents-mobilescheme:electric-agentsversion: frompackages/agents-mobile/package.jsonruntimeVersion: frompackages/agents-mobile/package.json, matching the native app version because we are not using EAS Update initiallyextra.eas.projectId:11a024df-c681-4374-867a-5c5905be9133owner:electric-axandroid.package:com.electricsql.agents.mobileandroid.versionCode: from CIios.bundleIdentifier:com.electricsql.agents.mobileios.buildNumber: from CIios.infoPlist.ITSAppUsesNonExemptEncryption:false, assuming the app only uses standard/exempt encryption such as HTTPS.
packages/agents-mobile/eas.json includes:
development: for local development builds if needed.preview: internal distribution, Android APK.preview-ios-simulator: unsigned iOS simulator build.canary: internal distribution Android APK.canary-store: store-submittable Android build targeting the Play internal track.production: store-submittable Android App Bundle.
Current shape:
{
"cli": {
"version": ">= 15.0.0",
"appVersionSource": "local"
},
"build": {
"preview": {
"node": "24.11.1",
"distribution": "internal",
"android": {
"buildType": "apk"
}
},
"preview-ios-simulator": {
"node": "24.11.1",
"distribution": "internal",
"ios": {
"simulator": true
}
},
"canary": {
"node": "24.11.1",
"distribution": "internal",
"android": {
"buildType": "apk"
}
},
"canary-store": {
"node": "24.11.1",
"distribution": "store"
},
"production": {
"node": "24.11.1",
"distribution": "store"
}
},
"submit": {
"canary-store": {
"android": {
"serviceAccountKeyPath": "./google-service-account.json",
"track": "internal"
}
},
"production": {
"android": {
"serviceAccountKeyPath": "./google-service-account.json",
"track": "production"
}
}
}
}Start with local app versioning because changesets already owns package versions. If we later want EAS remote versioning for versionCode / buildNumber, switch deliberately after confirming it does not fight the changesets release flow.
Scripts in packages/agents-mobile/package.json:
doctor: runsexpo-doctor.export:android:expo export --platform android --output-dir dist/androidexport:ios:expo export --platform ios --output-dir dist/iosci:check: typecheck, doctor, and Android export.
In GitHub Actions, call package scripts with pnpm --filter @electric-ax/agents-mobile run <script>. A bare pnpm --filter ... doctor is parsed as pnpm's own doctor command, not the package script.
Before making EAS builds required:
- Done: aligned
reactandreact-dombetweenagents-mobileandagents-server-ui. - Done: resolved the Expo SDK expected
react-native-webviewversion. - Done: aligned the runtime/server-ui TypeScript peer graph used by shared TanStack DB types.
- Done: re-ran
expo-doctor; it now passes 18/18 checks.
Mirror the desktop workflow structure:
.github/workflows/agents_mobile_pr.yml
.github/workflows/agents_mobile_canary.yml
.github/workflows/agents_mobile_build.yml
.github/workflows/agents_mobile_ios_simulator.yml
scripts/ci/mobile-affected.mjs
Model after scripts/ci/desktop-affected.mjs.
Inputs:
BASE_SHA
Global changes should trigger mobile builds:
.github/workflows/agents_mobile_*.yml.npmrc.tool-versionspackage.jsonpatches/**pnpm-lock.yamlpnpm-workspace.yamltsconfig.base.jsontsconfig.build.json
Workspace impact should include the mobile package closure:
@electric-ax/agents-mobile...
This should naturally catch changes in:
packages/agents-mobilepackages/agents-server-uipackages/agents-runtime- shared workspace dependencies used by mobile
Reusable workflow with inputs:
channel:pr,canary,stable, orios-simulatorversiongit_refplatform:android,ios, orallprofile:preview,preview-ios-simulator,canary,canary-store, orproductionsubmit: booleanrelease_tag: optionalrelease_name: optional
Steps:
- Checkout
git_ref. - Setup pnpm and Node from
.tool-versions. - Install mobile dependency closure.
- Build the mobile dependency closure so clean runners can resolve workspace exports.
- Run
pnpm --filter @electric-ax/agents-mobile run ci:check. - Run iOS export when the requested platform is
iosorall. - Setup EAS via
expo/expo-github-action. - If
submitis true for Android, writeGOOGLE_PLAY_SERVICE_ACCOUNT_JSONtopackages/agents-mobile/google-service-account.json. - Run
eas build --platform <platform> --profile <profile> --non-interactive --wait --json. - If
submitis true, run EAS Submit via--auto-submit. - For PRs, update a sticky PR comment with build status and EAS build URL.
Triggers:
pull_requestworkflow_dispatch
Behavior:
- Detect mobile impact.
- If impacted, run unauthenticated local checks for every PR.
- If impacted and repository secrets are available, call
agents_mobile_build.ymlwith:channel: prprofile: previewplatform: androidsubmit: falseversion: pr-<number>-<sha>
- If secrets are unavailable, skip EAS Build and comment that the PR needs a maintainer-triggered build.
PR artifact expectation:
- EAS internal distribution Android APK link.
- Sticky PR comment similar to desktop artifact comments.
Triggers:
- Push to
main workflow_dispatch
Behavior:
- Detect mobile impact.
- If impacted, call
agents_mobile_build.ymlwith:channel: canaryprofile: canaryplatform: androidsubmit: falseuntil Google Play submit is configuredversion: canary-<run_number>-<sha>for CI labeling, while app version still comes from package/config
Publishing target:
- EAS internal distribution first.
- Google Play internal track later via the
canary-storeprofile if we choose to enable canary store submission.
Extend the existing changesets job:
- Add outputs:
mobile_release_versionmobile_release_tag
- Capture
@electric-ax/agents-mobilefromsteps.changesets.outputs.publishedPackages. - Add a
publish-agents-mobilejob:- Needs
changesets. - Runs only when
mobile_release_tagis non-empty. - Calls
agents_mobile_build.yml. - Uses:
channel: stableprofile: productionplatform: androidgit_ref: ${{ needs.changesets.outputs.mobile_release_tag }}submit: true
- Needs
Manual workflow with inputs:
git_ref: optional branch, tag, or SHA to build.
Behavior:
- Calls
agents_mobile_build.ymlwith:channel: ios-simulatorprofile: preview-ios-simulatorplatform: iossubmit: false
- Requires
EXPO_TOKEN. - Does not require Apple Developer credentials.
This workflow is intentionally manual at first to control EAS build volume and because simulator artifacts are useful for targeted iOS smoke testing rather than every PR.
Changesets should continue to own semantic app versions.
Recommended:
package.json.versionis the app display version.app.config.tsreads that version.- Android
versionCodeis monotonically increasing and generated by CI or an explicit EAS versioning decision. - iOS
buildNumberfollows the same strategy once enabled.
Avoid tying Android versionCode directly to semver components unless we are confident it will never collide or decrease.
- Done: added
app.config.ts. - Done: added
eas.json. - Done: added final Android package id.
- Done: added reserved iOS bundle id.
- Done: added mobile CI/export scripts.
- Done: fixed
expo-doctorissues. - Done: confirmed:
- mobile typecheck
- Expo doctor
- Android export
- iOS export
- generated Expo public config
- Done: added
scripts/ci/mobile-affected.mjs. - Done: added reusable
agents_mobile_build.yml. - Done: added
agents_mobile_pr.yml. - Done: Android preview APK builds use EAS internal distribution.
- Done: PR workflow runs unauthenticated local checks for all impacted PRs.
- Done: same-repository/trusted PRs get a sticky EAS build comment; fork PRs get a skip comment.
- Done: added
agents_mobile_canary.yml. - Done: build Android from
mainusing the EAScanaryinternal distribution profile. - Not enabled yet: submit canary builds to Google Play internal track using the
canary-storeprofile andGOOGLE_PLAY_SERVICE_ACCOUNT_JSON. - iOS canaries remain blocked until Apple Developer/TestFlight setup is available.
- Done: extended
changesets_release.yml. - Done: capture mobile release tag/version from
publishedPackages. - Done: trigger Android production EAS build when
@electric-ax/agents-mobileis published. - Done: write
GOOGLE_PLAY_SERVICE_ACCOUNT_JSONto a temporary CI file for EAS Submit. - Done: submit stable Android builds to the Google Play production track through EAS Submit.
- Still possible: Google Play may require an initial manual AAB upload or additional store setup before the first API-based production submission succeeds.
Before the Apple Developer account is ready:
- Done: iOS export check passes locally and in mobile CI checks.
- Done: added
preview-ios-simulatorEAS profile. - Done: added manual
agents_mobile_ios_simulator.ymlworkflow. - Done: set
ITSAppUsesNonExemptEncryptiontofalseto avoid App Store Connect encryption metadata blocking simulator/TestFlight setup for standard HTTPS-only encryption. - Done: confirmed native iOS simulator compilation with EAS build
8b8a5b65-a189-4319-83ae-baaacca23f97.- Simulator artifact:
https://expo.dev/artifacts/eas/iepfJtpqS5QpRP1ktVDT8v.tar.gz
- Simulator artifact:
- Note: the manual
agents_mobile_ios_simulator.ymlworkflow can only be dispatched from GitHub after the workflow file exists on the default branch.
After the Apple Developer account is ready:
- Add bundle id and App Store Connect app.
- Configure EAS iOS credentials.
- Enable signed iOS preview/canary builds.
- Add TestFlight submission.
- Enable stable iOS release submission.
- Whether stable Android releases go directly to production or use staged rollout.
- Whether PR EAS builds should run on every impacted PR or require a label to control build volume/cost.
- Whether to keep
private: trueon@electric-ax/agents-mobile. Changesets can version/tag private packages in this repo, so this does not block release orchestration. - Whether to enable Google Play internal-track canary submission now or keep canaries on EAS internal distribution until after the first stable submission proves the Play path.
Immediate:
- Finish the remaining Google Play setup questionnaires/listing requirements so the first production submission is not blocked by Play Console metadata.
- Decide whether Android stable releases should submit directly to production or use a staged rollout/internal track first.
- Decide whether to enable
canary-storesubmissions to the Play internal track. - Land PR #4408 so the manual iOS simulator workflow exists on the default branch and can be dispatched from GitHub.
After Apple Developer access:
- Create the Bundle ID and App Store Connect app for
com.electricsql.agents.mobile. - Configure EAS-managed iOS signing credentials.
- Add signed iOS preview/canary/TestFlight profiles.
- Extend Changesets release publishing for iOS/TestFlight/App Store.