# EAS Update (over-the-air JS)

How to ship a **JS-only** change to installed builds **without an App Store round-trip**,
given Glide's setup (Expo SDK 56, `expo-updates`, `runtimeVersion` fingerprint policy).
Covers what's OTA-able, how to publish, the one constraint that decides whether a device
ever pulls your update, and how a device receives one.

What's already wired up:

- [`app.json`](../../app.json) — `runtimeVersion: { policy: "fingerprint" }` and
  `updates.url` (EAS project `h14h/glide`, projectId `f011c3fc-1e40-458c-a64c-42c258928a00`).
- [`eas.json`](../../eas.json) — each build profile carries a `channel`
  (`development` / `preview` / `production`).
- `expo-updates` is a dependency, so the native module ships in every build.

## The one constraint that drives everything

> **An update only applies if its `runtimeVersion` equals the installed build's
> `runtimeVersion`.**

That's the whole game. Publishing is easy; *matching* is where it goes wrong. Everything
below is in service of getting the runtime versions to line up.

## The model: build ↔ channel ↔ branch ↔ runtime version

- A **build** is stamped with two things: a **channel** (from its `eas.json` profile) and a
  **runtimeVersion** (computed at build time — see below).
- An **update** is published to a **branch**. A channel points at a **same-named branch** by
  default (our `preview` channel → `preview` branch).
- On launch, a build asks its channel for the **newest update on the pointed-to branch whose
  `runtimeVersion` matches the build's**. No match → nothing downloads (silently).

## How the runtime version is computed (and the trap)

Under the **fingerprint** policy, the runtime version is a hash
([`@expo/fingerprint`](https://github.com/expo/expo/tree/main/packages/expo-modules-autolinking))
of the inputs that determine the **native binary** — native dependencies, app config, config
plugins, `eas.json`, **and `.gitignore`** (source reason `bareGitIgnore`) — but **NOT your
app's JS/TS source.**

That's *by design*: it's what lets a JS-only change keep the same runtime version and be
OTA-eligible. But it has a sharp edge:

> A JS-only change is OTA-eligible **only from a clean working tree.** Any stray edit to a
> fingerprinted file — even **one line in `.gitignore`**, or a tweak to `eas.json` — shifts
> the runtime version off the build's. The update still uploads fine; it just **silently
> never matches**, so no device pulls it.

**The tell:** `eas update` prints the commit hash with a trailing **`*`** when the tree is
dirty. Treat a `*` as a red flag — stop and clean the tree before trusting the update.

## OTA-able vs needs a rebuild

| Change | Same fingerprint? | How to ship |
|---|---|---|
| JS/TS in `src/`, app logic, styling, copy | ✅ yes | **OTA** |
| Add / upgrade / remove a native dep (`package.json`) | ❌ no | rebuild |
| `app.json` / app config / config plugins | ❌ no | rebuild |
| `eas.json` | ❌ no | rebuild |
| `.gitignore` | ❌ no | rebuild — **or revert before publishing** |
| App-config assets (e.g. `icon.png`) | ⚠️ usually no | rebuild |

If a change needs a rebuild, the fingerprint policy *protects* you: the new update simply
won't match old builds, rather than shipping JS that expects native code that isn't there.

## Publish

From the branch holding the change, on a **clean tree**:

```bash
npx eas-cli update --branch preview --platform ios \
  --environment preview --non-interactive \
  --message "<what changed>"
```

- `--environment` is **required** in `--non-interactive` mode — use the env matching the build
  profile (so the EAS env vars baked into the bundle line up with the build's).
- `--platform ios` scopes the publish to iOS (Glide's only shipped platform today).

## Verify *before* trusting an OTA

1. **Fingerprint matches the build.**
   `npx expo-updates fingerprint:generate --platform ios` → its combined `hash` must equal the
   target build's **Runtime Version** from `npx eas-cli build:list --channel preview`.
2. **Publish output matches.** The `eas update` summary's `Runtime version` must equal that
   build's, and the `Commit` line must have **no `*`**.
3. **Channel points at the branch.** `npx eas-cli channel:view preview` should list branch
   `preview` with your update group as its most recent.

## How a device receives it

Default config (`enabled`, `checkAutomatically: ON_LOAD`, `fallbackToCacheTimeout: 0` — we set
none of these, so the defaults hold):

- On a **cold launch**, the app checks its branch+channel for a newer matching update and
  **downloads it in the background.** It does *not* interrupt the running session — it keeps
  the bundle it launched with.
- The downloaded update is applied on the **next** cold launch.

So to pull a freshly-published update onto a device:

> **launch** (downloads in background — still old) → **fully quit** → **relaunch** (now runs
> the new bundle).

Needs network on that first launch. Two relaunches, not one.

**Confirming what's actually running:** behaviorally (the change itself) is the quickest check.
At runtime, `expo-updates` also exposes `Updates.updateId` / `Updates.createdAt` /
`Updates.channel` / `Updates.runtimeVersion` — not surfaced in the UI today, but a tiny
diagnostic line could be added if "did it update?" ever needs a definitive answer.

## Rollback

The newest matching update on the branch wins, so to undo: **republish the previous good
bundle**, or use `eas update:rollback` to roll the channel back to a prior update group.

## Worked example — the Plan-transition fix

- **Build:** `main` @ `cfd1754`, runtime version `1b632d84…`, channel `preview`.
- **Fix:** `plan-anim` @ `9e60ce5` — JS-only (one file, `src/design/planning-screen.tsx`).
- **First publish, dirty tree** (a stray `.gitignore` edit) → runtime `7cb85f9e…` ≠ build →
  would never apply. The `eas update` output flagged it: `Commit 9e60ce5…*`.
- **Cleaned the tree, republished** → fingerprint `1b632d84…` == build → **applies.** Same JS,
  correct runtime version.

## Checklist

- [ ] Change is JS-only (see the table — anything native/config needs a rebuild)
- [ ] Working tree is **clean** (`git status` empty)
- [ ] On the branch holding the change
- [ ] `expo-updates fingerprint:generate --platform ios` hash == target build's Runtime Version
- [ ] `eas update --branch <ch> --platform ios --environment <env> --non-interactive -m "…"`
- [ ] Publish output: `Runtime version` matches the build, **no `*`** on the commit
- [ ] On device: launch → fully quit → relaunch → confirm the new behavior
