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.

  • Mobile & Desktop
  • $expo-eas-builds
  • 198 lines
  • 2 files
  • ~2,100 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- expo-eas-builds

Installs to ~/.agents/skills/expo-eas-builds/ for every project on this machine.

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

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.

~/.agents/skills/expo-eas-builds/SKILL.md

--- name: expo-eas-builds description: Configures and runs Expo Application Services - eas.json build profiles, EAS Build for development, preview and store binaries, EAS Submit to App… license: MIT ---

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" plus autoIncrement lets EAS own build numbers (buildNumber / versionCode), so two machines never upload the same number. Seed it once with eas build:version:set if 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 channel in 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) use secret visibility and no EXPO_PUBLIC_ prefix.
  • For EAS Update, the same --environment must 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

GoalCommand
Dev client for simulators/deviceseas build -p ios --profile development
Tester buildeas build -p all --profile preview
Store buildeas build -p all --profile production
Store build, then submiteas build -p ios --profile production --auto-submit
Reproduce a cloud failure locallyeas 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 credentials to 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 --clean plus a local native build.
  • EAS Workflows (.eas/workflows/*.yml) can chain fingerprint, build, submit and update jobs on Expo’s runners; GitHub Actions with expo/expo-github-action is 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.

PolicyRuntime changes whenChoose when
{ "policy": "fingerprint" }Any native-affecting input changes (deps, plugins, config)Default: safest automatic option
{ "policy": "appVersion" }You bump versionTeam reliably bumps version on native change
Fixed string "1.4.0"You edit itBare 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 appVersion policy but no version bump crashes old binaries on launch. Prefer the fingerprint policy.
  • autoIncrement with appVersionSource: "local" writes to app.json in CI; the change is lost unless committed. Use remote versioning.
  • Preview builds on the production channel 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 status before eas update in production.
  • Android APKs from buildType: apk cannot be uploaded to Play; production must be AAB.

Verification

  • eas build:list --limit 5 shows the expected profile, channel and runtime per build.
  • Install the preview build, publish a visible change to preview, cold-start twice, confirm it appears. Then eas update:rollback and 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.yamlgenerated from this page

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.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem