In eas.json, how does extends merge a child EAS build profile with its parent, and what can the child silently inherit?
answer
- child keys win, shallow spread
- env, android, ios merged one level
- platform object beats profile root
- defaults fill in last
- chain limit; missing parent errors
basics
~20 sWith extends, EAS CLI resolves the parent first, then overlays the child: child keys replace the parent's, while env, android and ios are merged key by key. Anything the child does not override, such as developmentClient or channel, is inherited.
solid answer
~50 sEAS CLI resolves `"extends": "parent"` recursively, then merges the child over the parent with a **shallow spread**: every top-level key the child sets replaces the parent's value. Three keys are merged one level deeper: `env` (variables combined, child wins per name) and the `android` and `ios` objects, which merge key by key the same way. After the chain is resolved, the platform object is merged over the profile root, and the schema defaults (`distribution: "store"`, `credentialsSource: "remote"`) fill in whatever is still unset. The chain is capped at five profiles, which also stops cycles, and extending a missing profile is an error. The trap is silent inheritance: a `preview` that extends `development` inherits `developmentClient: true` and any `ios.simulator: true`; a `preview` that extends `production` inherits its `channel`. A child can only neutralize a parent key by setting it explicitly, so I extend a neutral `base` profile instead of chaining real ones.
code
json · 13 lines{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": { "simulator": true }
},
"preview": {
"extends": "development",
"env": { "APP_VARIANT": "preview" }
}
}
}go deeper
Recall that extends makes one profile inherit another's settings, and that the child's own keys win.
Explain the merge: shallow for most keys, key by key for env, android and ios, platform over root, defaults last, a five-profile chain limit.
Show you spot silent inheritance of developmentClient, simulator, channel or environment, and restructure profiles around a neutral base with explicit per-profile keys.
Set a team convention for eas.json shape, such as one base and flat children, because each extra level multiplies what a reviewer must hold in their head.
## What extends is for An `eas.json` with several build profiles tends to repeat settings: the same Node version, the same image, the same shared variables. The `extends` key lets one profile inherit another's settings and override only what differs: ```json { "build": { "base": { "env": { "FEATURE_FLAGS": "remote" } }, "preview": { "extends": "base", "distribution": "internal" } } } ``` Understanding exactly how EAS CLI merges profiles is what separates using it from being surprised by it. ## How EAS CLI resolves a profile The resolver in the `eas-json` package works in this order: 1. **Look up the requested profile**, `production` if no `--profile` was passed. A missing profile is an error that lists the available names. 2. **Follow `extends` recursively.** Each parent is resolved first, then the child is merged over it. A missing parent raises an "Extending non-existent build profile" error. The resolver refuses chains deeper than five profiles, which also turns a cycle into an error instead of an infinite loop. 3. **Merge platform over root.** The resolved profile's `android` or `ios` object, depending on the platform being built, is merged over the profile's root keys, so platform-specific values win. 4. **Apply schema defaults underneath**, for example `distribution: "store"` and `credentialsSource: "remote"`, so they only fill keys nobody set. ## The merge rule itself | Key | How child and parent combine | |---|---| | Any scalar key (`distribution`, `channel`, `developmentClient`, `environment`, `node`) | Child value replaces parent value | | `env` | Combined; a name set in both takes the child's value | | `android`, `ios` | Merged key by key with the same rules, including their own `env` | | Any other object-valued key | Replaced as a whole by the child's object | Two consequences follow: - A child **cannot delete** a parent key. To turn off an inherited `developmentClient`, it must set `"developmentClient": false`; to stop a Simulator build, `"ios": { "simulator": false }`. - A parent's `env` variable is present in every descendant unless a descendant overrides that name. ## Silent inheritance, the real-world trap Chaining **real** profiles is where teams get hurt: - `preview` extends `development` to reuse its variables, and silently becomes a **development build** (`developmentClient: true`) or an iOS **Simulator build** (`ios.simulator: true`). Testers receive something that waits for a dev server or will not install on their phones. - `preview` extends `production` to reuse its settings, and inherits **`channel: "production"`**, so preview testers run whatever is published for store users, and a preview-only update can never reach them. - A shared parent sets `environment: "production"`, and every child loads production variables unless it overrides the field. Nothing warns about any of these, because each is a legitimate configuration. ## Resolving the trap step by step Take the first example below and build `preview` for iOS: 1. `development` has no parent, so it resolves to `developmentClient: true`, `distribution: "internal"` and `ios: { simulator: true }`. 2. `preview` is merged over it. It adds `env.APP_VARIANT` and overrides nothing else, so all three keys survive. 3. The `ios` object is merged over the root, so `simulator: true` applies to this iOS build. 4. Defaults fill `credentialsSource: "remote"`; `distribution` is already set. The result is a **Simulator development build** labelled "preview", which is exactly what nobody intended and nothing flagged. ## A safer structure - Create a **neutral `base` profile** that holds only what is truly shared: tool versions, the image, common non-secret `env` entries. - Have `development`, `preview` and `production` each extend `base` directly, and set `distribution`, `environment`, `channel` and `developmentClient` **explicitly** in each. - Use one extra level only for small variants, such as `development-simulator` extending `development` with `ios.simulator: true`. - Keep chains shallow; two levels is easy to reason about, the five-profile limit is not a target. ## Checking what a profile resolves to - EAS CLI prints the resolved environment and which `env` names came from the profile when a build starts; read that output on the first build of a new profile. - `EAS_BUILD_PROFILE` on the worker confirms which profile ran. - Review `eas.json` changes like code: a new `extends` line changes every key the child does not set.
- A child EAS profile needs to stop being a Simulator build that its parent defines; how does it do that?It sets the key explicitly: `"ios": { "simulator": false }`. The `ios` objects merge key by key with the child winning, but a key the child omits is inherited, so leaving it out does nothing. The cleaner fix is not to extend a profile that has simulator-only settings.
- In eas.json, what happens to env when a child profile and its parent both define it?The two objects are combined: names only in the parent are kept, names only in the child are added, and names in both take the child's value. That is the one top-level object EAS merges rather than replaces, alongside the `android` and `ios` platform objects.
extends works like a recipe card that says 'as the base recipe, except…': everything you do not strike out is still in the dish, including the ingredient you forgot the base recipe had.
saying these in an interview costs you the question
- A child profile's env replaces the parent's env object entirely.
- Leaving a key out of the child profile resets it to the default.
- extends copies only env and tool versions, not developmentClient or channel.
- Circular extends chains are resolved by taking the first profile.
- Root-level keys override the same key inside the ios object.