Claude Skill

mobile-deployment-eas

EAS Build, Update, Submit deployment patterns

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_mobile-deployment-eas_skills_mobile-deployment-eas-3a51ef5.zip · 18 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/mobile-deployment-eas/skills/mobile-deployment-eas
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

EAS Deployment Patterns

Quick Guide: EAS (Expo Application Services) handles cloud builds, OTA updates, and app store submission. Use build profiles in eas.json to separate development/preview/production builds. Use EAS Update with runtime version fingerprinting for safe OTA deploys. Use --environment flag (SDK 55+) instead of --channel when publishing updates. Let EAS manage credentials unless you have enterprise requirements.


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST use the --environment flag with eas update on SDK 55+ projects -- the --channel flag is replaced)

(You MUST set runtimeVersion with "fingerprint" policy for projects with native dependencies -- mismatched runtime versions crash apps on OTA update)

(You MUST use EAS Secrets for sensitive values -- NEVER put API keys or tokens in eas.json env blocks or EXPO_PUBLIC_ variables)

(You MUST run eas build from the app directory in monorepos -- NOT from the repository root)

</critical_requirements>


Auto-detection: EAS Build, EAS Update, EAS Submit, eas.json, eas build, eas update, eas submit, eas credentials, eas secret, EAS Workflows, build profiles, OTA updates, runtime version, fingerprint policy, app store submission, code signing, eas-cli

When to use:

  • Configuring cloud builds for iOS and Android
  • Publishing OTA (over-the-air) updates to deployed apps
  • Submitting builds to App Store or Google Play
  • Managing iOS provisioning profiles and Android keystores
  • Setting up CI/CD pipelines for mobile deployment
  • Configuring build profiles for different environments

Key patterns covered:

  • eas.json build profiles with inheritance (extends)
  • EAS Update channels, branches, and runtime versions
  • EAS Submit for iOS App Store and Google Play
  • Credentials management (automatic vs local)
  • EAS Secrets for sensitive build-time values
  • EAS Workflows for CI/CD automation
  • Monorepo build configuration
  • Version management with autoIncrement and appVersionSource

When NOT to use:

  • General Expo SDK development (app.config.ts, Expo Router, components)
  • Local-only builds with npx expo run:ios/android without EAS
  • Projects not using Expo managed workflow



Detailed Resources:

  • examples/core.md - Build profiles, eas.json configuration, runtime versions, submit config, monorepo setup
  • examples/updates.md - OTA update workflows, client-side update hook, channel strategies
  • examples/credentials.md - iOS provisioning, Android keystores, local credentials, code signing
  • examples/workflows.md - EAS Workflows YAML, custom build steps, GitHub integration
  • reference.md - Decision frameworks, CLI commands, version management, anti-patterns

<red_flags>

RED FLAGS

High Priority Issues:

  • Using --channel on SDK 55+ projects -- replaced by --environment flag; old flag no longer works
  • Missing runtimeVersion in app config -- OTA updates fail silently or crash on incompatible builds
  • Putting secrets in eas.json env blocks -- config is committed to version control; use EAS Secrets instead
  • Running eas build from monorepo root -- must run from the app directory (apps/mobile/, not repo root)
  • Not setting autoIncrement for production -- Android versionCode must strictly increase; Play Store rejects same or lower values

Medium Priority Issues:

  • Using distribution: "store" for preview builds -- use "internal" for ad hoc/internal distribution
  • Not configuring resourceClass -- default may be slow for large projects; "medium" or "large" speeds up builds
  • Skipping --non-interactive in CI -- EAS prompts for input by default; CI pipelines hang without this flag
  • Forgetting --auto-submit when build + submit are always paired -- saves a manual step and avoids submitting the wrong build

Gotchas & Edge Cases:

  • fingerprint policy can be too aggressive -- may flag changes that don't affect native code, forcing unnecessary rebuilds
  • iOS provisioning profiles expire after 12 months -- won't affect apps in production, but next build requires regeneration via eas credentials
  • autoIncrement: "version" vs "buildNumber" -- "version" bumps the user-visible version string; "buildNumber" bumps the internal build number only
  • EAS Update has ~50MB asset limit -- large assets should use a CDN, not be bundled in updates
  • appVersionSource: "remote" requires paid plan -- tracks versions server-side; free tier must manage locally
  • Android buildType: "apk" is for testing only -- Play Store requires "app-bundle" (AAB) for production submissions
  • Code signing (end-to-end) requires Production or Enterprise plan -- not available on free tier
  • extends depth limit is 5 levels -- circular dependencies cause build errors
  • iOS simulator builds cannot install on physical devices -- need a separate development-device profile with "simulator": false

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md

(You MUST use the --environment flag with eas update on SDK 55+ projects -- the --channel flag is replaced)

(You MUST set runtimeVersion with "fingerprint" policy for projects with native dependencies -- mismatched runtime versions crash apps on OTA update)

(You MUST use EAS Secrets for sensitive values -- NEVER put API keys or tokens in eas.json env blocks or EXPO_PUBLIC_ variables)

(You MUST run eas build from the app directory in monorepos -- NOT from the repository root)

Failure to follow these rules will cause OTA update crashes, credential exposure, and build failures.

</critical_reminders>

Files (skills)
  • examples
    • core.md 8.7 KB
      # Core EAS Patterns
      
      > Build profiles, eas.json configuration, runtime versions, submit config, and monorepo setup. See [SKILL.md](../SKILL.md) for decisions and philosophy.
      
      ---
      
      ## Build Profiles
      
      ### Minimal eas.json
      
      ```json
      {
        "cli": {
          "version": ">= 14.0.0"
        },
        "build": {
          "development": {
            "developmentClient": true,
            "distribution": "internal",
            "ios": {
              "simulator": true
            },
            "android": {
              "buildType": "apk"
            }
          },
          "preview": {
            "distribution": "internal",
            "channel": "preview"
          },
          "production": {
            "channel": "production"
          }
        },
        "submit": {
          "production": {
            "ios": {
              "appleId": "your@email.com",
              "ascAppId": "1234567890"
            },
            "android": {
              "serviceAccountKeyPath": "./google-services.json",
              "track": "internal"
            }
          }
        }
      }
      ```
      
      ### Complete eas.json with Profile Inheritance
      
      ```json
      {
        "cli": {
          "version": ">= 14.0.0",
          "appVersionSource": "remote"
        },
        "build": {
          "base": {
            "node": "20.17.0",
            "env": {
              "EXPO_PUBLIC_APP_ENV": "development"
            }
          },
          "development": {
            "extends": "base",
            "developmentClient": true,
            "distribution": "internal",
            "ios": {
              "simulator": true,
              "resourceClass": "m-medium"
            },
            "android": {
              "buildType": "apk"
            }
          },
          "development-device": {
            "extends": "development",
            "ios": {
              "simulator": false
            }
          },
          "preview": {
            "extends": "base",
            "distribution": "internal",
            "channel": "preview",
            "env": {
              "EXPO_PUBLIC_APP_ENV": "preview"
            }
          },
          "production": {
            "extends": "base",
            "autoIncrement": "buildNumber",
            "channel": "production",
            "env": {
              "EXPO_PUBLIC_APP_ENV": "production"
            },
            "ios": {
              "resourceClass": "m-medium"
            },
            "android": {
              "buildType": "app-bundle"
            }
          }
        },
        "submit": {
          "production": {
            "ios": {
              "appleId": "your@email.com",
              "ascAppId": "1234567890",
              "appleTeamId": "ABC123DEF"
            },
            "android": {
              "serviceAccountKeyPath": "./google-services.json",
              "track": "internal",
              "releaseStatus": "draft"
            }
          }
        }
      }
      ```
      
      **Why good:** `base` profile shares Node version and env config, `extends` eliminates duplication, `development-device` overrides only `simulator: false` from `development`, production uses `autoIncrement` and AAB
      
      ### Build Commands
      
      ```bash
      # Development builds
      eas build --profile development --platform ios        # iOS simulator
      eas build --profile development --platform android    # Android APK
      eas build --profile development-device --platform ios # iOS physical device
      
      # Preview builds (internal distribution)
      eas build --profile preview --platform all
      
      # Production builds
      eas build --profile production --platform all
      eas build --profile production --platform all --auto-submit  # Build + submit
      
      # Install on simulator/emulator after build completes
      eas build:run --platform ios
      eas build:run --platform android
      ```
      
      ---
      
      ## Runtime Versions
      
      ### Fingerprint Policy (Recommended for Most Projects)
      
      ```typescript
      // app.config.ts
      export default {
        runtimeVersion: {
          policy: "fingerprint",
        },
        updates: {
          url: `https://u.expo.dev/${process.env.EAS_PROJECT_ID}`,
        },
      };
      ```
      
      **How fingerprint works:** Hashes all native dependencies, config plugins, and native code modifications. When the hash changes (new native module added, SDK version bumped), the runtime version changes automatically. OTA updates only deploy to builds with a matching fingerprint.
      
      ### AppVersion Policy (Simpler Projects)
      
      ```typescript
      // app.config.ts
      const APP_VERSION = "1.2.0";
      
      export default {
        version: APP_VERSION,
        runtimeVersion: {
          policy: "appVersion",
        },
        updates: {
          url: `https://u.expo.dev/${process.env.EAS_PROJECT_ID}`,
        },
      };
      ```
      
      **How appVersion works:** Uses the `version` field from app config as the runtime version. You must manually bump `version` when making native changes.
      
      ### Custom Runtime Version (Full Control)
      
      ```typescript
      // app.config.ts
      export default {
        runtimeVersion: "2.0.0",
        updates: {
          url: `https://u.expo.dev/${process.env.EAS_PROJECT_ID}`,
        },
      };
      ```
      
      **When to use:** When you need exact control and understand the consequences. You must manually update the string when native code changes.
      
      ---
      
      ## Submit Configuration
      
      ### iOS App Store
      
      ```json
      {
        "submit": {
          "production": {
            "ios": {
              "appleId": "your@email.com",
              "ascAppId": "1234567890",
              "appleTeamId": "ABC123DEF"
            }
          }
        }
      }
      ```
      
      For CI (avoids 2FA prompts), use App Store Connect API keys:
      
      ```json
      {
        "submit": {
          "production": {
            "ios": {
              "ascApiKeyPath": "./AuthKey_XXXXXXXXXX.p8",
              "ascApiKeyIssuerId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
              "ascApiKeyId": "XXXXXXXXXX"
            }
          }
        }
      }
      ```
      
      **Gotcha:** Store the `.p8` file path relative to `eas.json`. In CI, download the key from EAS Secrets or your secret manager before the submit step.
      
      ### Google Play
      
      ```json
      {
        "submit": {
          "production": {
            "android": {
              "serviceAccountKeyPath": "./google-services.json",
              "track": "internal",
              "releaseStatus": "draft"
            }
          }
        }
      }
      ```
      
      **Track options:**
      
      | Track        | Purpose               |
      | ------------ | --------------------- |
      | `internal`   | Internal testers only |
      | `alpha`      | Closed testing        |
      | `beta`       | Open testing          |
      | `production` | Public release        |
      
      **Tip:** Start with `"track": "internal"` and promote through Play Console. Set `"releaseStatus": "draft"` to review before publishing.
      
      ### Staged Rollouts (Android)
      
      ```json
      {
        "submit": {
          "production": {
            "android": {
              "serviceAccountKeyPath": "./google-services.json",
              "track": "production",
              "releaseStatus": "inProgress",
              "rollout": 0.1
            }
          }
        }
      }
      ```
      
      **How it works:** `rollout: 0.1` releases to 10% of users. Increase through Play Console as confidence grows.
      
      ---
      
      ## Environment Variables in Builds
      
      ### Non-Sensitive Variables (eas.json)
      
      ```json
      {
        "build": {
          "preview": {
            "env": {
              "EXPO_PUBLIC_API_URL": "https://staging.api.example.com",
              "EXPO_PUBLIC_APP_ENV": "preview"
            }
          },
          "production": {
            "env": {
              "EXPO_PUBLIC_API_URL": "https://api.example.com",
              "EXPO_PUBLIC_APP_ENV": "production"
            }
          }
        }
      }
      ```
      
      ### Sensitive Variables (EAS Secrets)
      
      ```bash
      # Create project-scoped secrets
      eas secret:create --scope project --name MY_AUTH_TOKEN --value "tok_abc123"
      eas secret:create --scope project --name MAPS_API_KEY --value "AIza..."
      
      # Create account-scoped file secret (shared across projects)
      eas secret:create --scope account --name GOOGLE_SERVICES_JSON --type file --value ./google-services.json
      ```
      
      Secrets are automatically available as environment variables during builds -- no `eas.json` env configuration needed.
      
      ---
      
      ## Monorepo Setup
      
      ### Directory Structure
      
      ```
      my-monorepo/
        apps/
          mobile/              <-- eas.json lives here
            eas.json
            app.config.ts
            package.json
          web/
        packages/
          shared/
          ui/
        package.json           <-- Monorepo root
        pnpm-workspace.yaml    <-- Or workspaces in package.json
      ```
      
      ### Running Builds
      
      ```bash
      # ALWAYS run from the app directory
      cd apps/mobile
      eas build --profile preview --platform ios
      
      # NOT from the monorepo root
      # cd my-monorepo && eas build  <-- WRONG
      ```
      
      ### Monorepo Considerations
      
      - EAS auto-detects your package manager and runs install from the monorepo root
      - Lock file must be at the repository root (EAS looks there, not in the app dir)
      - For pnpm, ensure `pnpm-workspace.yaml` exists at the repo root
      - If shared packages need building before the app, add a `postinstall` script:
      
      ```json
      {
        "scripts": {
          "postinstall": "cd ../.. && pnpm run build --filter=@myorg/shared"
        }
      }
      ```
      
      ---
      
      ## Version Management
      
      ### Remote Version Source
      
      ```json
      {
        "cli": {
          "appVersionSource": "remote"
        }
      }
      ```
      
      **How it works:** EAS tracks version numbers server-side. No need to update `app.config.ts` version fields manually. Requires a paid EAS plan.
      
      ### Manual Version Sync
      
      ```typescript
      // app.config.ts
      const APP_VERSION = "1.2.0";
      const BUILD_NUMBER = 42;
      
      export default {
        version: APP_VERSION,
        ios: {
          buildNumber: String(BUILD_NUMBER), // MUST be string for iOS
        },
        android: {
          versionCode: BUILD_NUMBER, // MUST be integer for Android
        },
      };
      ```
      
      **Key rules:**
      
      - iOS `buildNumber` is a **string** -- always use `String()`
      - Android `versionCode` is an **integer** -- never stringify
      - Android `versionCode` must **strictly increase** for each Play Store upload
      
    • credentials.md 6 KB
      # EAS Credentials Patterns
      
      > iOS provisioning, Android keystores, local credentials, and code signing. See [SKILL.md](../SKILL.md) for decisions and philosophy.
      
      ---
      
      ## Automatic Credential Management (Recommended)
      
      EAS manages all signing credentials by default. On first build, it generates and stores:
      
      - **iOS:** Distribution Certificate + Provisioning Profile
      - **Android:** Upload keystore + key
      
      ```bash
      # First build -- EAS prompts to generate credentials
      eas build --profile production --platform ios
      # "Would you like to let EAS manage your credentials? (Y/n)"
      # Select Y -- EAS creates and stores everything
      
      # Subsequent builds reuse stored credentials
      eas build --profile production --platform ios
      # No prompts -- credentials are already managed
      ```
      
      ### Team Collaboration
      
      After credentials are generated, teammates with EAS dashboard access can build without Apple Developer Portal access:
      
      ```bash
      # Teammate runs build -- uses remote credentials
      eas build --profile preview --platform ios
      # No Apple Developer login needed
      ```
      
      ### Credential Inspection
      
      ```bash
      # View current credentials
      eas credentials --platform ios
      eas credentials --platform android
      
      # Options:
      # - View existing certificates/profiles
      # - Generate new ones
      # - Remove old ones
      # - Switch between managed and local
      ```
      
      ---
      
      ## iOS Provisioning
      
      ### Distribution Types
      
      | Type       | Use Case                   | Build Profile Setting              |
      | ---------- | -------------------------- | ---------------------------------- |
      | App Store  | App Store submission       | `distribution: "store"` (default)  |
      | Ad Hoc     | Internal testing (devices) | `distribution: "internal"`         |
      | Enterprise | Company-wide distribution  | Enterprise Apple Developer account |
      
      ### Device Registration for Ad Hoc
      
      Internal distribution (preview builds) requires registering test devices:
      
      ```bash
      # Register a device -- generates a URL for the device owner to visit
      eas device:create
      
      # List registered devices
      eas device:list
      
      # After adding new devices, rebuild to include them in provisioning profile
      eas build --profile preview --platform ios
      ```
      
      **Gotcha:** iOS provisioning profiles expire after 12 months. This doesn't affect apps already installed, but the next build will need a new profile. Run `eas credentials --platform ios` to regenerate.
      
      ### App Store Connect API Keys (CI)
      
      For CI pipelines, use API keys to avoid 2FA prompts:
      
      1. Generate a key in [App Store Connect > Users and Access > Integrations > App Store Connect API](https://appstoreconnect.apple.com/access/integrations/api)
      2. Download the `.p8` file
      3. Store the key as an EAS Secret or in your CI's secret manager
      
      ```json
      {
        "submit": {
          "production": {
            "ios": {
              "ascApiKeyPath": "./AuthKey_XXXXXXXXXX.p8",
              "ascApiKeyIssuerId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
              "ascApiKeyId": "XXXXXXXXXX"
            }
          }
        }
      }
      ```
      
      ---
      
      ## Android Keystore
      
      ### Upload Keystore (Play App Signing)
      
      Google Play App Signing is the recommended approach. Google manages the app signing key; you use an upload key:
      
      ```bash
      # EAS generates upload keystore on first build
      eas build --profile production --platform android
      
      # If you need to view or reset:
      eas credentials --platform android
      ```
      
      **Key distinction:**
      
      - **App signing key:** Google manages this. Used to sign the APK/AAB delivered to users.
      - **Upload key:** You (EAS) manage this. Used to sign uploads to Play Console. Can be reset if compromised.
      
      ### Keystore Backup
      
      ```bash
      # Download your keystore from EAS (for backup)
      eas credentials --platform android
      # Select: "Download credentials from EAS servers"
      ```
      
      **Store securely** -- if you lose the upload keystore and don't use Play App Signing, you cannot update your app on the Play Store.
      
      ---
      
      ## Local Credential Management
      
      For enterprises or teams that must manage their own signing credentials:
      
      ### credentials.json
      
      Create `credentials.json` at the app root (same level as `eas.json`):
      
      ```json
      {
        "ios": {
          "provisioningProfilePath": "./certs/profile.mobileprovision",
          "distributionCertificate": {
            "path": "./certs/dist-cert.p12",
            "password": "DIST_CERT_PASSWORD"
          }
        },
        "android": {
          "keystore": {
            "keystorePath": "./certs/upload-keystore.jks",
            "keystorePassword": "KEYSTORE_PASSWORD",
            "keyAlias": "upload-key",
            "keyPassword": "KEY_PASSWORD"
          }
        }
      }
      ```
      
      ### Enable Local Credentials in Build Profile
      
      ```json
      {
        "build": {
          "production": {
            "credentialsSource": "local"
          }
        }
      }
      ```
      
      **Security rules:**
      
      - Add `credentials.json` to `.gitignore`
      - Store actual credential files outside of version control
      - Use environment variables for passwords (reference via `$VARIABLE_NAME` syntax)
      - In CI, download credentials from a secret manager before the build step
      
      ### Generating Local Credentials Manually
      
      ```bash
      # iOS: Export from Apple Developer Portal or Keychain Access
      # - Distribution Certificate (.p12)
      # - Provisioning Profile (.mobileprovision)
      
      # Android: Generate keystore
      keytool -genkeypair -v \
        -storetype JKS \
        -keyalg RSA \
        -keysize 2048 \
        -validity 10000 \
        -storepass YOUR_STORE_PASSWORD \
        -keypass YOUR_KEY_PASSWORD \
        -alias upload-key \
        -keystore upload-keystore.jks \
        -dname "CN=Your Name, OU=Your Org, O=Your Company, L=City, S=State, C=US"
      ```
      
      ---
      
      ## EXPO_TOKEN for CI Authentication
      
      Authenticate EAS CLI in CI without interactive login:
      
      ```bash
      # Generate token at https://expo.dev/accounts/[account]/settings/access-tokens
      # Store as CI secret (GitHub Actions, CircleCI, etc.)
      
      # CI pipeline uses token automatically
      EXPO_TOKEN=your-token eas build --profile production --platform all --non-interactive
      ```
      
      ### GitHub Actions Example
      
      ```yaml
      - name: Setup EAS
        uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
      
      - name: Build
        run: eas build --profile production --platform all --non-interactive
      ```
      
      The `EXPO_TOKEN` environment variable is read automatically by EAS CLI -- no `eas login` needed.
      
    • updates.md 7.3 KB
      # EAS Update Patterns
      
      > OTA update workflows, client-side update hooks, and channel strategies. See [SKILL.md](../SKILL.md) for decisions and philosophy.
      
      ---
      
      ## Publishing Updates
      
      ### SDK 55+ (--environment flag required)
      
      ```bash
      # Publish to preview environment
      eas update --environment preview --message "Fix login crash"
      
      # Publish to production environment
      eas update --environment production --message "Version 1.2.1 hotfix"
      
      # List published updates
      eas update:list
      
      # Rollback a bad update
      eas update:rollback --channel production
      ```
      
      ### SDK 54 and Earlier (--channel flag)
      
      ```bash
      eas update --channel preview --message "Fix login crash"
      eas update --channel production --message "Version 1.2.1 hotfix"
      ```
      
      **Migration note:** If upgrading from SDK 54 to 55, replace all `--channel` flags with `--environment` in your scripts and CI pipelines.
      
      ---
      
      ## Channel-Branch-Update Model
      
      ```
      Channel: "production"  ──points to──>  Branch: "production"
                                                |
                                                +-- Update 3 (latest - ACTIVE)
                                                +-- Update 2
                                                +-- Update 1
      
      Channel: "preview"     ──points to──>  Branch: "preview"
                                                |
                                                +-- Update 2 (latest - ACTIVE)
                                                +-- Update 1
      ```
      
      **Key concepts:**
      
      - A **channel** is assigned to a build profile (in `eas.json`)
      - A **branch** contains a list of updates (most recent is active)
      - By default, a channel points to a branch with the same name
      - You can remap: `eas channel:edit production --branch hotfix-v2`
      
      ### Remapping for Hotfixes
      
      ```bash
      # Normal state: production channel -> production branch
      # Hotfix scenario:
      eas update --branch hotfix-v2 --message "Critical security fix"
      eas channel:edit production --branch hotfix-v2
      
      # After hotfix is merged into main:
      eas update --environment production --message "Includes hotfix"
      eas channel:edit production --branch production  # Reset
      ```
      
      ---
      
      ## Client-Side Update Hook
      
      Check for and apply updates in the app. Skip checks in development mode.
      
      ```typescript
      // hooks/use-ota-updates.ts
      import * as Updates from "expo-updates";
      import { useEffect, useState } from "react";
      import { Alert } from "react-native";
      
      const UPDATE_CHECK_INTERVAL_MS = 30_000;
      
      interface UpdateState {
        isChecking: boolean;
        isAvailable: boolean;
        isDownloading: boolean;
      }
      
      export function useOTAUpdates() {
        const [state, setState] = useState<UpdateState>({
          isChecking: false,
          isAvailable: false,
          isDownloading: false,
        });
      
        const checkForUpdates = async () => {
          if (__DEV__) return; // Skip in development builds
      
          try {
            setState((prev) => ({ ...prev, isChecking: true }));
            const update = await Updates.checkForUpdateAsync();
      
            if (update.isAvailable) {
              setState((prev) => ({
                ...prev,
                isAvailable: true,
                isDownloading: true,
              }));
              await Updates.fetchUpdateAsync();
              setState((prev) => ({ ...prev, isDownloading: false }));
      
              Alert.alert(
                "Update Ready",
                "A new version has been downloaded. Restart to apply.",
                [
                  { text: "Later", style: "cancel" },
                  { text: "Restart", onPress: () => Updates.reloadAsync() },
                ],
              );
            }
          } catch (error) {
            // Fail silently -- app continues with current version
            console.error("Error checking for updates:", error);
          } finally {
            setState((prev) => ({ ...prev, isChecking: false }));
          }
        };
      
        useEffect(() => {
          checkForUpdates();
      
          const interval = setInterval(checkForUpdates, UPDATE_CHECK_INTERVAL_MS);
          return () => clearInterval(interval);
        }, []);
      
        return {
          ...state,
          checkForUpdates,
        };
      }
      ```
      
      **Usage in root layout:**
      
      ```typescript
      // app/_layout.tsx
      import { useOTAUpdates } from "../hooks/use-ota-updates";
      
      export default function RootLayout() {
        useOTAUpdates(); // Checks on mount and periodically
      
        return <Stack />;
      }
      ```
      
      **Why good:** Named constant for interval, graceful error handling (app continues on failure), skip in dev mode, cleanup on unmount
      
      ---
      
      ## Immediate Update Strategy
      
      Force the update before the user interacts with the app. Use `fallbackToCacheTimeout` in app config.
      
      ```typescript
      // app.config.ts
      export default {
        updates: {
          url: `https://u.expo.dev/${process.env.EAS_PROJECT_ID}`,
          fallbackToCacheTimeout: 5000, // Wait up to 5s for update on launch
        },
        runtimeVersion: {
          policy: "fingerprint",
        },
      };
      ```
      
      **How it works:** On app launch, `expo-updates` checks for a new update. If one is found and downloaded within 5 seconds, it runs immediately. Otherwise, the cached version runs and the update downloads in the background for the next launch.
      
      **Trade-off:** Setting a high timeout delays app startup. Setting `0` (default) always uses the cached version and downloads updates in the background.
      
      ---
      
      ## Channel Strategy
      
      ```
      Environment     Channel        Build Profile    Use Case
      -----------     -------        -------------    --------
      Development     development    development      Local dev testing
      Preview/QA      preview        preview          Internal testing
      Production      production     production       App store users
      
      Git Branch      Deployment Target
      ----------      -----------------
      feature/*       development channel (or none)
      staging         preview channel
      main            production channel
      ```
      
      ### Aligning eas.json with Channels
      
      ```json
      {
        "build": {
          "development": {
            "developmentClient": true,
            "distribution": "internal",
            "channel": "development"
          },
          "preview": {
            "distribution": "internal",
            "channel": "preview"
          },
          "production": {
            "channel": "production"
          }
        }
      }
      ```
      
      **Key rule:** Each build profile's `channel` determines which updates it receives. A production build with `"channel": "production"` will never receive updates published to the `preview` channel.
      
      ---
      
      ## Update Size Considerations
      
      - EAS Update has a **~50MB limit** per update bundle
      - Large assets (images, videos, fonts) should be hosted on a CDN, not bundled
      - SDK 55 introduces **Hermes bytecode diffing** (opt-in via `enableBsdiffPatchSupport` in `expo-build-properties`) for up to 75% smaller OTA downloads
      - Only JavaScript and asset changes are included in updates -- native code changes require a new build
      
      ---
      
      ## Code Signing (End-to-End)
      
      For apps requiring cryptographic verification of updates (enterprise, regulated industries):
      
      ```bash
      # Generate signing key pair (one-time)
      npx expo-updates codesigning:generate \
        --key-output-directory keys \
        --certificate-output-directory certs \
        --certificate-common-name "My App"
      
      # Configure in app.config.ts
      export default {
        updates: {
          codeSigningCertificate: "./certs/certificate.pem",
          codeSigningMetadata: {
            keyid: "main-key",
            alg: "rsa-v1_5-sha256",
          },
        },
      };
      
      # Publish signed update
      eas update --environment production \
        --private-key-path ./keys/private-key.pem \
        --message "Signed update v1.2.1"
      ```
      
      **Key rules:**
      
      - Private key never leaves your machine (or CI secret store)
      - Public key is embedded in the app via config
      - Requires EAS Production or Enterprise plan
      - Updates without valid signatures are rejected by the client
      
    • workflows.md 6 KB
      # EAS Workflows Patterns
      
      > CI/CD automation with EAS Workflows YAML, custom build steps, and GitHub integration. See [SKILL.md](../SKILL.md) for decisions and philosophy.
      
      ---
      
      ## Workflow Basics
      
      EAS Workflows are defined in `.eas/workflows/*.yaml` files. They orchestrate builds, tests, submissions, and notifications in a single pipeline.
      
      ### Directory Structure
      
      ```
      my-app/
        .eas/
          workflows/
            deploy-production.yaml
            preview-build.yaml
            nightly.yaml
        eas.json
        app.config.ts
      ```
      
      ---
      
      ## Complete Production Deployment Workflow
      
      ```yaml
      # .eas/workflows/deploy-production.yaml
      name: Deploy Production
      
      on:
        push:
          branches: [main]
      
      jobs:
        build_ios:
          type: build
          params:
            platform: ios
            profile: production
      
        build_android:
          type: build
          params:
            platform: android
            profile: production
      
        submit_ios:
          needs: [build_ios]
          type: submit
          params:
            platform: ios
      
        submit_android:
          needs: [build_android]
          type: submit
          params:
            platform: android
      
        notify:
          needs: [submit_ios, submit_android]
          if: ${{ success() }}
          type: slack
          params:
            webhook_url: ${{ env.SLACK_HOOK_URL }}
            message: "Production build submitted for review"
      ```
      
      **Why good:** `needs` enforces ordering (submit only after build succeeds), `if: ${{ success() }}` prevents notification on failure, both platforms build in parallel
      
      ---
      
      ## Preview Build on Pull Request
      
      ```yaml
      # .eas/workflows/preview-build.yaml
      name: Preview Build
      
      on:
        pull_request:
          branches: [main]
          types: [opened, synchronize]
      
      jobs:
        build_preview:
          type: build
          params:
            platform: all
            profile: preview
      ```
      
      **Tip:** Add `[eas skip]` to a commit message to skip triggered workflow runs.
      
      ---
      
      ## Workflow with Custom Steps
      
      Pre-packaged jobs use `type`. Custom jobs use `steps` with shell commands and built-in functions.
      
      ```yaml
      # .eas/workflows/test-and-build.yaml
      name: Test and Build
      
      on:
        push:
          branches: [main]
      
      defaults:
        tools:
          node: "20"
          pnpm: "9"
      
      jobs:
        test:
          steps:
            - uses: eas/checkout
            - uses: eas/install_node_modules
            - name: Run type check
              run: pnpm tsc --noEmit
            - name: Run tests
              run: pnpm test
      
        build:
          needs: [test]
          type: build
          params:
            platform: all
            profile: production
      ```
      
      **Key built-in functions:**
      
      | Function                   | Purpose                                     |
      | -------------------------- | ------------------------------------------- |
      | `eas/checkout`             | Clone repository source                     |
      | `eas/install_node_modules` | Install deps (auto-detects package manager) |
      | `eas/prebuild`             | Run `expo prebuild`                         |
      | `eas/download_build`       | Retrieve build artifacts                    |
      | `eas/restore_cache`        | Restore cached files                        |
      | `eas/save_cache`           | Save files to cache                         |
      | `eas/use_npm_token`        | Configure private npm access                |
      | `eas/send_slack_message`   | Post to Slack webhook                       |
      
      ---
      
      ## Sharing Data Between Jobs
      
      Use `set-output` and `set-env` shell functions to pass data between steps and jobs.
      
      ```yaml
      jobs:
        version:
          outputs:
            app_version: ${{ steps.get_version.outputs.value }}
          steps:
            - uses: eas/checkout
            - id: get_version
              name: Extract version
              run: |
                VERSION=$(node -p "require('./package.json').version")
                set-output value "$VERSION"
      
        build:
          needs: [version]
          type: build
          env:
            APP_VERSION: ${{ needs.version.outputs.app_version }}
          params:
            platform: all
            profile: production
      ```
      
      **`set-output`** shares across steps and jobs (via `outputs`). **`set-env`** shares within the same job only.
      
      ---
      
      ## Scheduled Workflows
      
      ```yaml
      # .eas/workflows/nightly.yaml
      name: Nightly Build
      
      on:
        schedule:
          - cron: "0 2 * * 1-5" # 2 AM UTC, Monday-Friday
      
      jobs:
        build:
          type: build
          params:
            platform: all
            profile: preview
      ```
      
      ---
      
      ## Manual Trigger with Inputs
      
      ```yaml
      # .eas/workflows/manual-deploy.yaml
      name: Manual Deploy
      
      on:
        workflow_dispatch:
          inputs:
            platform:
              type: choice
              options: [ios, android, all]
            profile:
              type: choice
              options: [preview, production]
      
      jobs:
        build:
          type: build
          params:
            platform: ${{ inputs.platform }}
            profile: ${{ inputs.profile }}
      ```
      
      ---
      
      ## Concurrency Control
      
      Prevent multiple workflow runs from overlapping:
      
      ```yaml
      name: Deploy
      on:
        push:
          branches: [main]
      
      concurrency:
        group: deploy-${{ github.ref }}
        cancel-in-progress: true # Cancel previous run if new push arrives
      
      jobs:
        build:
          type: build
          params:
            platform: all
            profile: production
      ```
      
      ---
      
      ## GitHub Actions Alternative
      
      If you prefer GitHub Actions over EAS Workflows:
      
      ```yaml
      # .github/workflows/eas-build.yml
      name: EAS Build
      
      on:
        push:
          branches: [main]
        pull_request:
          branches: [main]
      
      jobs:
        build:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
      
            - uses: actions/setup-node@v4
              with:
                node-version: "20"
                cache: "npm"
      
            - name: Install dependencies
              run: npm ci
      
            - name: Setup EAS
              uses: expo/expo-github-action@v8
              with:
                eas-version: latest
                token: ${{ secrets.EXPO_TOKEN }}
      
            - name: Build Preview
              if: github.event_name == 'pull_request'
              run: eas build --profile preview --platform all --non-interactive
      
            - name: Build and Submit Production
              if: github.ref == 'refs/heads/main'
              run: eas build --profile production --platform all --auto-submit --non-interactive
      ```
      
      **Trade-offs:**
      
      - **EAS Workflows:** Tightly integrated with EAS services, pre-packaged job types, less boilerplate
      - **GitHub Actions:** More ecosystem tooling, familiar to most teams, works with non-EAS steps
      
      Both approaches use `EXPO_TOKEN` for authentication and `--non-interactive` to prevent CI hangs.
      
  • reference.md 8.2 KB
    # EAS Reference
    
    > Decision frameworks, CLI commands, and version management. Reference from [SKILL.md](SKILL.md).
    
    ---
    
    ## Decision Frameworks
    
    ### Build Profile Selection
    
    ```
    Which build profile?
    |
    +-> Local development testing?
    |   +-> On simulator/emulator -> development (simulator: true)
    |   +-> On physical device -> development-device (simulator: false)
    |
    +-> Testing with real team / QA?
    |   +-> preview (distribution: "internal")
    |
    +-> App store submission?
    |   +-> production (autoIncrement, distribution: "store")
    |
    +-> CI/CD builds?
        +-> PR builds -> preview
        +-> Main branch -> production
    ```
    
    ### Runtime Version Policy
    
    ```
    Choosing runtimeVersion policy?
    |
    +-> App has native dependencies that change often?
    |   +-> YES -> "fingerprint" (auto-detects native changes)
    |   +-> NO  -> Continue...
    |
    +-> Simple app, minimal native dependencies?
    |   +-> "appVersion" (tracks version field)
    |
    +-> Need exact control over compatibility?
    |   +-> Custom string (e.g., "1.0.0")
    |
    +-> Default recommendation?
        +-> "fingerprint" for safety, "appVersion" for simplicity
    ```
    
    ### Channel Strategy
    
    ```
    Which channel for this update?
    |
    +-> Development builds (testing locally)?
    |   +-> development channel (or none)
    |
    +-> Internal testing / QA (preview builds)?
    |   +-> preview channel
    |
    +-> App store releases?
    |   +-> production channel
    |
    +-> Hotfix for production?
        +-> production channel (same as affected build)
    ```
    
    ### Credentials Source
    
    ```
    How to manage signing credentials?
    |
    +-> First-time setup / small team?
    |   +-> Automatic (let EAS manage everything)
    |
    +-> Enterprise with existing certificates?
    |   +-> Local (credentials.json with own signing files)
    |
    +-> Team members need to build independently?
    |   +-> Automatic (remote credentials shared via EAS)
    |
    +-> CI/CD pipeline?
        +-> Automatic (EXPO_TOKEN for auth, EAS manages signing)
    ```
    
    ---
    
    ## CLI Commands Quick Reference
    
    ### Build
    
    ```bash
    eas build --profile [profile] --platform [ios|android|all]
    eas build --profile production --platform all --auto-submit
    eas build --profile production --platform all --non-interactive  # CI
    eas build:list                     # List recent builds
    eas build:view [id]                # View build details
    eas build:run --platform [platform]  # Run built app on simulator/emulator
    ```
    
    ### Update (SDK 55+)
    
    ```bash
    eas update --environment [development|preview|production] --message "description"
    eas update:list                    # List published updates
    eas update:rollback --channel [channel]  # Rollback to previous update
    eas channel:edit [channel] --branch [branch]  # Remap channel to branch
    eas channel:list                   # List channels
    ```
    
    ### Update (SDK 54 and earlier)
    
    ```bash
    eas update --channel [channel] --message "description"
    ```
    
    ### Submit
    
    ```bash
    eas submit --platform [ios|android]
    eas submit --platform [platform] --id [build-id]  # Submit specific build
    ```
    
    ### Credentials and Secrets
    
    ```bash
    eas credentials --platform [ios|android]  # Interactive management
    eas device:create                  # Register iOS test device
    eas device:list                    # List registered devices
    eas secret:create --scope [project|account] --name [name] --value [value]
    eas secret:create --scope account --name [name] --type file --value ./path
    eas secret:list
    eas secret:delete --name [name]
    ```
    
    ### Workflows
    
    ```bash
    eas workflow:run [workflow-file]    # Trigger workflow manually
    eas workflow:list                  # List workflow runs
    ```
    
    ---
    
    ## Version Management
    
    ### autoIncrement Options
    
    | Value           | iOS Effect             | Android Effect         | When to Use                |
    | --------------- | ---------------------- | ---------------------- | -------------------------- |
    | `false`         | No change              | No change              | Manual version management  |
    | `true`          | Increments buildNumber | Increments versionCode | Default auto-increment     |
    | `"buildNumber"` | Increments buildNumber | Increments versionCode | Internal build tracking    |
    | `"version"`     | Increments version     | Increments versionName | User-visible version bumps |
    
    ### appVersionSource
    
    | Value      | Behavior                                | Plan Required |
    | ---------- | --------------------------------------- | ------------- |
    | `"local"`  | Reads version from app config (default) | Free          |
    | `"remote"` | EAS tracks version server-side          | Paid          |
    
    ### Version Number Rules
    
    - iOS `buildNumber`: Must be a **string** (e.g., `"42"`)
    - Android `versionCode`: Must be an **integer** (e.g., `42`)
    - Android versionCode must **strictly increase** for each Play Store upload
    - iOS buildNumber must increase per App Store submission (per bundle ID)
    
    ---
    
    ## eas.json Schema Overview
    
    ```
    eas.json
    |
    +-> cli
    |   +-> version           # Minimum EAS CLI version
    |   +-> appVersionSource  # "local" | "remote"
    |
    +-> build
    |   +-> [profile-name]
    |       +-> extends              # Inherit from another profile
    |       +-> distribution         # "store" | "internal"
    |       +-> developmentClient    # true for dev builds
    |       +-> channel              # EAS Update channel
    |       +-> environment          # "development" | "preview" | "production"
    |       +-> env                  # Build-time environment variables
    |       +-> node / yarn / pnpm / bun  # Tool versions
    |       +-> autoIncrement        # Version bump strategy
    |       +-> resourceClass        # "default" | "medium" | "large"
    |       +-> credentialsSource    # "remote" | "local"
    |       +-> prebuildCommand      # Custom prebuild override
    |       +-> cache                # { disabled, key, paths }
    |       +-> config               # Custom workflow file
    |       +-> ios
    |       |   +-> simulator        # true for simulator builds
    |       |   +-> scheme           # Xcode scheme name
    |       |   +-> buildConfiguration  # "Release" | "Debug"
    |       |   +-> resourceClass    # Platform-specific machine size
    |       |   +-> image            # Build environment image
    |       +-> android
    |           +-> buildType        # "app-bundle" | "apk"
    |           +-> gradleCommand    # Custom Gradle task
    |           +-> ndk              # Android NDK version
    |           +-> resourceClass    # Platform-specific machine size
    |           +-> image            # Build environment image
    |
    +-> submit
        +-> [profile-name]
            +-> ios
            |   +-> appleId          # Apple ID email
            |   +-> ascAppId         # App Store Connect ID
            |   +-> appleTeamId      # Developer Team ID
            |   +-> ascApiKeyPath    # API key for CI (avoids 2FA)
            |   +-> ascApiKeyIssuerId
            |   +-> ascApiKeyId
            +-> android
                +-> serviceAccountKeyPath  # Google service account JSON
                +-> track            # "internal" | "alpha" | "beta" | "production"
                +-> releaseStatus    # "completed" | "draft" | "halted" | "inProgress"
                +-> rollout          # Fraction 0-1 for staged rollouts
    ```
    
    ---
    
    ## Anti-Patterns
    
    > Summary red flags are in [SKILL.md](SKILL.md). These are detailed anti-patterns with explanations.
    
    ### Anti-Pattern 1: Secrets in Version Control
    
    ```json
    {
      "build": {
        "production": {
          "env": {
            "MY_AUTH_TOKEN": "tok_abc123..."
          }
        }
      }
    }
    ```
    
    **Why wrong:** `eas.json` is committed to version control. Anyone with repo access sees the token.
    
    **Fix:** Use EAS Secrets: `eas secret:create --scope project --name MY_AUTH_TOKEN --value "tok_abc123..."`. The secret is injected as an env var during build.
    
    ---
    
    ### Anti-Pattern 2: Missing Non-Interactive in CI
    
    ```yaml
    # CI pipeline
    - run: eas build --profile production --platform all
      # Hangs waiting for user input!
    ```
    
    **Why wrong:** EAS CLI prompts for confirmations by default. CI has no stdin.
    
    **Fix:** Always add `--non-interactive`:
    
    ```yaml
    - run: eas build --profile production --platform all --non-interactive
    ```
    
    ---
    
    ### Anti-Pattern 3: APK for Production
    
    ```json
    {
      "build": {
        "production": {
          "android": {
            "buildType": "apk"
          }
        }
      }
    }
    ```
    
    **Why wrong:** Google Play requires Android App Bundles (AAB). APKs are for local testing / side-loading only.
    
    **Fix:** Use `"app-bundle"` (default) or omit `buildType` entirely for production:
    
    ```json
    {
      "build": {
        "production": {
          "android": {
            "buildType": "app-bundle"
          }
        }
      }
    }
    ```
    
  • SKILL.md 14.2 KB
    ---
    name: mobile-deployment-eas
    description: EAS Build, Update, Submit deployment patterns
    ---
    
    # EAS Deployment Patterns
    
    > **Quick Guide:** EAS (Expo Application Services) handles cloud builds, OTA updates, and app store submission. Use build profiles in `eas.json` to separate development/preview/production builds. Use EAS Update with runtime version fingerprinting for safe OTA deploys. Use `--environment` flag (SDK 55+) instead of `--channel` when publishing updates. Let EAS manage credentials unless you have enterprise requirements.
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Using This Skill
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST use the `--environment` flag with `eas update` on SDK 55+ projects -- the `--channel` flag is replaced)**
    
    **(You MUST set `runtimeVersion` with `"fingerprint"` policy for projects with native dependencies -- mismatched runtime versions crash apps on OTA update)**
    
    **(You MUST use EAS Secrets for sensitive values -- NEVER put API keys or tokens in `eas.json` env blocks or `EXPO_PUBLIC_` variables)**
    
    **(You MUST run `eas build` from the app directory in monorepos -- NOT from the repository root)**
    
    </critical_requirements>
    
    ---
    
    **Auto-detection:** EAS Build, EAS Update, EAS Submit, eas.json, eas build, eas update, eas submit, eas credentials, eas secret, EAS Workflows, build profiles, OTA updates, runtime version, fingerprint policy, app store submission, code signing, eas-cli
    
    **When to use:**
    
    - Configuring cloud builds for iOS and Android
    - Publishing OTA (over-the-air) updates to deployed apps
    - Submitting builds to App Store or Google Play
    - Managing iOS provisioning profiles and Android keystores
    - Setting up CI/CD pipelines for mobile deployment
    - Configuring build profiles for different environments
    
    **Key patterns covered:**
    
    - `eas.json` build profiles with inheritance (`extends`)
    - EAS Update channels, branches, and runtime versions
    - EAS Submit for iOS App Store and Google Play
    - Credentials management (automatic vs local)
    - EAS Secrets for sensitive build-time values
    - EAS Workflows for CI/CD automation
    - Monorepo build configuration
    - Version management with `autoIncrement` and `appVersionSource`
    
    **When NOT to use:**
    
    - General Expo SDK development (app.config.ts, Expo Router, components)
    - Local-only builds with `npx expo run:ios/android` without EAS
    - Projects not using Expo managed workflow
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    EAS separates **building**, **updating**, and **submitting** into distinct services that compose into a deployment pipeline. The key insight is that most mobile deployment complexity lives in credentials, versioning, and environment management -- EAS automates all three.
    
    **Core principles:**
    
    1. **Profiles over flags** -- Define build configurations in `eas.json`, not as CLI arguments scattered across scripts
    2. **Channels isolate environments** -- Production, preview, and development builds receive only their intended updates
    3. **Fingerprint prevents crashes** -- Runtime version fingerprinting auto-detects native changes so OTA updates never land on incompatible builds
    4. **Credentials are managed** -- Let EAS handle signing unless you have enterprise requirements; it generates, stores, and renews certificates automatically
    5. **Secrets stay server-side** -- Sensitive values live in EAS Secrets, never in config files or client-side variables
    
    **Mental model:**
    
    `eas.json` is your deployment configuration hub. Build profiles define HOW to build (development client, APK vs AAB, simulator vs device). Channels define WHERE updates go. Runtime versions define WHAT is compatible. Secrets define access to external services during builds.
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: Build Profiles with Inheritance
    
    Use `extends` to share configuration between profiles. Define a `base` profile for shared settings, then extend for each environment.
    
    ```json
    {
      "build": {
        "base": {
          "node": "20.17.0",
          "env": { "EXPO_PUBLIC_APP_ENV": "development" }
        },
        "development": {
          "extends": "base",
          "developmentClient": true,
          "distribution": "internal",
          "ios": { "simulator": true },
          "android": { "buildType": "apk" }
        },
        "production": {
          "extends": "base",
          "autoIncrement": "buildNumber",
          "channel": "production",
          "env": { "EXPO_PUBLIC_APP_ENV": "production" }
        }
      }
    }
    ```
    
    **Why good:** `extends` eliminates duplication across profiles, `autoIncrement` handles version bumps automatically, environment-specific env vars prevent mixing configurations
    
    > Full profile examples with all options: [examples/core.md](examples/core.md) - Build Profiles section
    
    ---
    
    ### Pattern 2: Runtime Versions and Fingerprinting
    
    Runtime version determines which builds are compatible with which OTA updates. The `"fingerprint"` policy auto-detects native changes.
    
    ```typescript
    // app.config.ts
    export default {
      runtimeVersion: {
        policy: "fingerprint", // Auto-detects native code changes
      },
      updates: {
        url: `https://u.expo.dev/${process.env.EAS_PROJECT_ID}`,
      },
    };
    ```
    
    | Policy        | Behavior                       | Best For                         |
    | ------------- | ------------------------------ | -------------------------------- |
    | `fingerprint` | Hashes all native dependencies | Complex apps with native modules |
    | `appVersion`  | Uses `version` from app config | Simple apps, manual control      |
    | Custom string | Exact match (e.g., `"1.0.0"`)  | Full manual control              |
    
    **Gotcha:** `fingerprint` can be aggressive -- it may flag changes that don't actually affect native code, requiring more builds than necessary. Use `appVersion` for simpler projects.
    
    > Full configuration examples: [examples/core.md](examples/core.md) - Runtime Versions section
    
    ---
    
    ### Pattern 3: Publishing OTA Updates
    
    Updates go to a channel, which points to a branch. Builds pull updates from their assigned channel.
    
    ```bash
    # SDK 55+ uses --environment (REQUIRED)
    eas update --environment production --message "Fix login crash"
    
    # SDK 54 and earlier uses --channel
    eas update --channel production --message "Fix login crash"
    ```
    
    **Channel-branch relationship:** By default, a channel auto-links to a branch with the same name. You can reassign: `eas channel:edit production --branch hotfix-v2`.
    
    **Rollback:** If a bad update ships, roll back immediately:
    
    ```bash
    eas update:rollback --channel production
    ```
    
    > Full update workflow with client-side hook: [examples/updates.md](examples/updates.md)
    
    ---
    
    ### Pattern 4: App Store Submission
    
    EAS Submit handles both iOS App Store and Google Play submission. Use `--auto-submit` to chain build and submit.
    
    ```bash
    # Submit latest build
    eas submit --platform ios
    eas submit --platform android
    
    # Build and submit in one step
    eas build --profile production --platform ios --auto-submit
    
    # Submit a specific build by ID
    eas submit --platform ios --id [build-id]
    ```
    
    ```json
    {
      "submit": {
        "production": {
          "ios": {
            "appleId": "your@email.com",
            "ascAppId": "1234567890",
            "appleTeamId": "ABC123DEF"
          },
          "android": {
            "serviceAccountKeyPath": "./google-services.json",
            "track": "internal",
            "releaseStatus": "draft"
          }
        }
      }
    }
    ```
    
    **Why good:** `track: "internal"` starts with internal testing before promoting, `releaseStatus: "draft"` gives manual control over publishing in Play Console
    
    > Full submission configuration: [examples/core.md](examples/core.md) - Submit Configuration section
    
    ---
    
    ### Pattern 5: Credentials Management
    
    Let EAS manage credentials by default. Use `eas credentials` to inspect or override.
    
    ```bash
    # Interactive credential management
    eas credentials --platform ios
    eas credentials --platform android
    
    # Register iOS test devices for internal distribution
    eas device:create
    eas device:list
    ```
    
    **Automatic management** (recommended): EAS creates Distribution Certificates, Provisioning Profiles (iOS), and keystores (Android) automatically on first build. Teammates with EAS access can build without Apple Developer credentials.
    
    **Local credentials** (enterprise): Use `credentials.json` at the app root with paths to your own signing files. Set `"credentialsSource": "local"` in the build profile.
    
    > Full credentials patterns: [examples/credentials.md](examples/credentials.md)
    
    ---
    
    ### Pattern 6: EAS Secrets
    
    Store sensitive build-time values server-side. Secrets are injected as environment variables during builds.
    
    ```bash
    # Project-scoped secret
    eas secret:create --scope project --name MY_AUTH_TOKEN --value "your-token"
    
    # Account-scoped secret (shared across projects)
    eas secret:create --scope account --name GOOGLE_SERVICES_JSON --type file --value ./google-services.json
    
    # List and manage
    eas secret:list
    eas secret:delete --name MY_AUTH_TOKEN
    ```
    
    Secrets are available as environment variables in the build process -- no `eas.json` configuration needed.
    
    **Key distinction:** `EXPO_PUBLIC_*` variables are embedded in the JS bundle (visible to users). EAS Secrets are build-time only (never in the bundle).
    
    ---
    
    ### Pattern 7: EAS Workflows (CI/CD)
    
    Define complete CI/CD pipelines in `.eas/workflows/*.yaml`. Workflows orchestrate builds, tests, submissions, and notifications.
    
    ```yaml
    # .eas/workflows/deploy-production.yaml
    name: Deploy Production
    on:
      push:
        branches: [main]
    
    jobs:
      build_ios:
        type: build
        params:
          platform: ios
          profile: production
    
      build_android:
        type: build
        params:
          platform: android
          profile: production
    
      submit_ios:
        needs: [build_ios]
        type: submit
        params:
          platform: ios
    
      submit_android:
        needs: [build_android]
        type: submit
        params:
          platform: android
    ```
    
    **Why good:** Single YAML defines the entire deployment pipeline, `needs` enforces job ordering, pre-packaged job types (`build`, `submit`) eliminate boilerplate
    
    > Full workflow examples with custom steps: [examples/workflows.md](examples/workflows.md)
    
    ---
    
    ### Pattern 8: Monorepo Builds
    
    Run EAS CLI from the app directory, not the repo root. Each app has its own `eas.json`.
    
    ```
    my-monorepo/
      apps/
        mobile/          <-- Run eas build from HERE
          eas.json
          app.config.ts
          package.json
      packages/
        shared/
      package.json       <-- NOT from here
    ```
    
    ```bash
    cd apps/mobile
    eas build --profile preview --platform ios
    ```
    
    **Gotcha:** EAS auto-detects your package manager (npm, pnpm, Yarn, Bun) and installs from the monorepo root. If detection fails with pnpm, ensure your `pnpm-workspace.yaml` is at the repo root.
    
    > Full monorepo configuration: [examples/core.md](examples/core.md) - Monorepo section
    
    </patterns>
    
    ---
    
    **Detailed Resources:**
    
    - [examples/core.md](examples/core.md) - Build profiles, eas.json configuration, runtime versions, submit config, monorepo setup
    - [examples/updates.md](examples/updates.md) - OTA update workflows, client-side update hook, channel strategies
    - [examples/credentials.md](examples/credentials.md) - iOS provisioning, Android keystores, local credentials, code signing
    - [examples/workflows.md](examples/workflows.md) - EAS Workflows YAML, custom build steps, GitHub integration
    - [reference.md](reference.md) - Decision frameworks, CLI commands, version management, anti-patterns
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    **High Priority Issues:**
    
    - **Using `--channel` on SDK 55+ projects** -- replaced by `--environment` flag; old flag no longer works
    - **Missing `runtimeVersion` in app config** -- OTA updates fail silently or crash on incompatible builds
    - **Putting secrets in `eas.json` env blocks** -- config is committed to version control; use EAS Secrets instead
    - **Running `eas build` from monorepo root** -- must run from the app directory (`apps/mobile/`, not repo root)
    - **Not setting `autoIncrement` for production** -- Android versionCode must strictly increase; Play Store rejects same or lower values
    
    **Medium Priority Issues:**
    
    - **Using `distribution: "store"` for preview builds** -- use `"internal"` for ad hoc/internal distribution
    - **Not configuring `resourceClass`** -- default may be slow for large projects; `"medium"` or `"large"` speeds up builds
    - **Skipping `--non-interactive` in CI** -- EAS prompts for input by default; CI pipelines hang without this flag
    - **Forgetting `--auto-submit` when build + submit are always paired** -- saves a manual step and avoids submitting the wrong build
    
    **Gotchas & Edge Cases:**
    
    - **`fingerprint` policy can be too aggressive** -- may flag changes that don't affect native code, forcing unnecessary rebuilds
    - **iOS provisioning profiles expire after 12 months** -- won't affect apps in production, but next build requires regeneration via `eas credentials`
    - **`autoIncrement: "version"` vs `"buildNumber"`** -- `"version"` bumps the user-visible version string; `"buildNumber"` bumps the internal build number only
    - **EAS Update has ~50MB asset limit** -- large assets should use a CDN, not be bundled in updates
    - **`appVersionSource: "remote"` requires paid plan** -- tracks versions server-side; free tier must manage locally
    - **Android `buildType: "apk"` is for testing only** -- Play Store requires `"app-bundle"` (AAB) for production submissions
    - **Code signing (end-to-end) requires Production or Enterprise plan** -- not available on free tier
    - **`extends` depth limit is 5 levels** -- circular dependencies cause build errors
    - **iOS simulator builds cannot install on physical devices** -- need a separate `development-device` profile with `"simulator": false`
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## CRITICAL REMINDERS
    
    > **All code must follow project conventions in CLAUDE.md**
    
    **(You MUST use the `--environment` flag with `eas update` on SDK 55+ projects -- the `--channel` flag is replaced)**
    
    **(You MUST set `runtimeVersion` with `"fingerprint"` policy for projects with native dependencies -- mismatched runtime versions crash apps on OTA update)**
    
    **(You MUST use EAS Secrets for sensitive values -- NEVER put API keys or tokens in `eas.json` env blocks or `EXPO_PUBLIC_` variables)**
    
    **(You MUST run `eas build` from the app directory in monorepos -- NOT from the repository root)**
    
    **Failure to follow these rules will cause OTA update crashes, credential exposure, and build failures.**
    
    </critical_reminders>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related