Expo EAS Builds skill for Codex
Ship binaries and OTA updates that match. Codex sets up eas.json so every binary has its own channel and environment, picks the fingerprint runtime policy so native changes never ride an OTA update, and publishes production updates as small percentage rollouts with a rollback rehearsed on the preview channel. Store submissions and production publishes wait for your confirmation.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- expo-eas-builds Installs to ~/.agents/skills/expo-eas-builds/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install expo-eas-builds The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project expo-eas-builds Installs to .agents/skills/expo-eas-builds/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/expo-eas-builds.zipunzip -o expo-eas-builds.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) expo-eas-builds PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $expo-eas-builds, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Configures and runs Expo Application Services - eas.json build profiles, EAS Build for development, preview and store binaries, EAS Submit to App Store Connect and Google Play, EAS Update over-the-air releases with channels, branches, runtimeVersion policies, percentage rollouts and rollbacks, plus EAS environment variables and CI tokens. Use when setting up eas.json, shipping a TestFlight or Play internal build, publishing or rolling back an OTA update, or debugging an update that devices never receive.
Use it when
- Creating or reworking eas.json build and submit profiles
- Shipping a TestFlight or Play internal build from EAS
- Publishing, rolling out or rolling back an EAS Update
- Devices are not receiving an over-the-air update
Use something else when
- You are building with fastlane on your own CI runners: use mobile-ci-fastlane
- The issue is App Store review or TestFlight setup itself: use ios-app-store-release
How to check the result
eas build:list shows the expected channel and runtime per build, and a visible change published to the preview channel appears after two cold starts, then disappears after a rollback.
The mistake it prevents
Publishing an OTA update after a native dependency change while the runtime version stays the same, which crashes every existing binary on launch.
Done well means: each binary comes from a named profile with a fixed channel and environment, OTA updates only ever reach binaries with a matching native runtime, and every production publish has a rollout plan and a tested rollback.
Inspect first
cat AGENTS.md 2>/dev/null
cat eas.json
npx expo config --type public | grep -iE 'runtimeVersion|updates|version'
npx eas-cli@latest whoami # needs network and a logged-in account or EXPO_TOKEN
EAS commands talk to Expo’s servers. In a sandbox with network off, prepare config and commands, then ask the user to run them or approve network access.
Profiles
{
"cli": { "version": ">= 16.0.0", "appVersionSource": "remote" },
"build": {
"base": { "node": "22.14.0" },
"development": {
"extends": "base",
"developmentClient": true,
"distribution": "internal",
"environment": "development",
"channel": "development"
},
"preview": {
"extends": "base",
"distribution": "internal",
"environment": "preview",
"channel": "preview",
"android": { "buildType": "apk" }
},
"production": {
"extends": "base",
"environment": "production",
"channel": "production",
"autoIncrement": true
}
},
"submit": {
"production": {
"ios": { "ascAppId": "1234567890" },
"android": { "track": "internal", "releaseStatus": "draft" }
}
}
}
appVersionSource: "remote"plusautoIncrementlets EAS own build numbers (buildNumber/versionCode), so two machines never upload the same number. Seed it once witheas build:version:setif the store already has higher numbers.distribution: "internal"produces ad hoc / APK builds for testers; iOS internal builds need registered devices (eas device:create).- Keep
channelin the build profile. The channel is baked into the binary and decides which updates it can receive.
Environment variables and secrets
eas env:create --environment production --name SENTRY_AUTH_TOKEN --value "..." --visibility secret
eas env:list --environment preview
eas env:pull --environment development # writes .env.local for local dev
EXPO_PUBLIC_*values are inlined into the JS bundle and visible to anyone; use them only for public config. Build-time secrets (Sentry upload tokens, private registry tokens) usesecretvisibility and noEXPO_PUBLIC_prefix.- For EAS Update, the same
--environmentmust be passed when publishing, or the update bundle is built with different values from the binary. - Never print secret values in logs or reports; do not paste them into chat.
Builds
| Goal | Command |
|---|---|
| Dev client for simulators/devices | eas build -p ios --profile development |
| Tester build | eas build -p all --profile preview |
| Store build | eas build -p all --profile production |
| Store build, then submit | eas build -p ios --profile production --auto-submit |
| Reproduce a cloud failure locally | eas build -p android --profile production --local |
| CI (non-interactive) | EXPO_TOKEN=... eas build -p all --profile production --non-interactive --no-wait |
- Let EAS manage iOS credentials unless the team already has a certificate process
(
eas credentialsto inspect). Android: back up the upload keystore before anything else; losing it means a Play upload-key reset. - Cloud build failing? Open the build log phase that failed (install, prebuild, pods,
Gradle, Xcode). Most failures reproduce with
npx expo prebuild --cleanplus a local native build. - EAS Workflows (
.eas/workflows/*.yml) can chain fingerprint, build, submit and update jobs on Expo’s runners; GitHub Actions withexpo/expo-github-actionis the other common route.
Submit
eas submit -p ios --latest --profile production # App Store Connect -> TestFlight
eas submit -p android --latest --profile production # Play track from eas.json
Submitting uploads to the store under the user’s account. Confirm with the user before
running submit, and before changing any Android track to production or releaseStatus
to completed. iOS uses an App Store Connect API key; Android needs a Google service
account JSON with release permissions. Store-side release steps belong to
$ios-app-store-release and $android-play-release.
OTA updates
runtimeVersion is the safety interlock
An update is delivered only to binaries whose runtimeVersion matches exactly.
| Policy | Runtime changes when | Choose when |
|---|---|---|
{ "policy": "fingerprint" } | Any native-affecting input changes (deps, plugins, config) | Default: safest automatic option |
{ "policy": "appVersion" } | You bump version | Team reliably bumps version on native change |
Fixed string "1.4.0" | You edit it | Bare projects with their own process |
eas fingerprint:compare shows what changed between two builds. If the fingerprint
changed, the change needs a new binary, not an update.
What an update may contain
JS, styles, images and other assets. Not: new native modules, permission strings, app icon, splash, bundle ID, entitlements, or SDK upgrades. Updates must also stay within store rules on downloaded code: bug fixes and incremental features, not a different app.
Publish, roll out, roll back
eas update --channel production --environment production \
--message "Fix checkout total rounding" --rollout-percentage 10
eas update:edit # raise the rollout percentage
eas update:revert-update-rollout # abandon a rollout in progress
eas update:rollback # roll a branch back to a previous or the embedded update
eas update:list --branch production
Production publishes are outward-facing: confirm channel, message and rollout percentage with the user first. Start at 10% or less, watch crash and error rates for the new update group, then raise.
Check the app actually applies updates: by default expo-updates fetches on launch and
applies on the next cold start. For critical fixes, call Updates.checkForUpdateAsync(),
Updates.fetchUpdateAsync(), then prompt and Updates.reloadAsync().
Debugging “the update never arrives”
- [ ] Binary's channel (build profile) matches the channel you published to
- [ ] Channel points at the branch you published (eas channel:view production)
- [ ] runtimeVersion of the update equals the binary's (eas update:list shows both)
- [ ] Platform included (did you publish with -p ios only?)
- [ ] App was cold-started twice (download, then apply)
- [ ] Not a debug/dev build: development builds load from the dev server, not EAS Update
- [ ] expo-updates is installed and updates.url is in the resolved config
Gotchas
- Bumping a native dependency and publishing an OTA update with
appVersionpolicy but no version bump crashes old binaries on launch. Prefer the fingerprint policy. autoIncrementwithappVersionSource: "local"writes to app.json in CI; the change is lost unless committed. Use remote versioning.- Preview builds on the
productionchannel receive production updates (and vice versa). Never share channels between tester and store builds. - An update published from a dirty working tree ships uncommitted code. Require a clean
git statusbeforeeas updatein production. - Android APKs from
buildType: apkcannot be uploaded to Play; production must be AAB.
Verification
eas build:list --limit 5shows the expected profile, channel and runtime per build.- Install the preview build, publish a visible change to
preview, cold-start twice, confirm it appears. Theneas update:rollbackand confirm it disappears. - For production updates, compare error rate of the new update group against the previous one before raising the rollout above the first step.
Report template
**Profile:** production (channel production, environment production, runtime fingerprint a1b2c3)
**Builds:** iOS 2.4.0 (312) and Android 2.4.0 (10312), EAS build IDs ...
**Submitted:** iOS to TestFlight; Android to internal track as draft (user confirmed)
**Update:** "Fix checkout total rounding", 10% rollout, group ID ...
**Rollback plan:** eas update:revert-update-rollout, tested on preview channel What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Expo EAS Builds"
short_description: "Ship binaries and OTA updates that match"
default_prompt: "$expo-eas-builds Set up eas.json with development, preview and production profiles" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.