expo / expo/eas-cli

eas deploy --prod reports "Promoted deployment to production" and exits 0 while production keeps serving the previous bundle

Open
#4,388 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
1.4k
Forks
236
Avg merge
3d 1h
Merged PRs (30d)
91

Description

### Summary

`eas deploy --prod` prints `✔ Promoted deployment to production` and exits 0 while production continues to serve the **previous** bundle. The upload half succeeds; the promotion half silently does not. Re-running the identical command promotes it.

This began as an occasional failure and now reproduces on **every** deploy of this project — four deploys on 2026-09-11, all four needed two runs.

### Managed or bare?

Managed. Expo Router web export (static output) on EAS Hosting.

### Environment

```
expo-env-info 2.1.0 environment info:
System:
OS: macOS 26.6.1
Binaries:
Node: 24.18.0
npm: 11.16.0
npmPackages:
expo: ~57.0.8 => 57.0.8
expo-router: ~57.0.8 => 57.0.8
react: 19.2.3 => 19.2.3
react-dom: 19.2.3 => 19.2.3
react-native: 0.86.0 => 0.86.0
react-native-web: ~0.21.0 => 0.21.2
Expo Workflow: managed

eas-cli/24.3.0 darwin-arm64 node-v24.18.0
```

### Error output

There is no error output. That is the substance of the report — the command claims to have done something it has not done:

```
✔ Created deployment
- Promoting deployment to production
✔ Promoted deployment to production

🎉 Your deployment is ready

Deployment URL https://warp-nexus--y3iw49p3qz.expo.app
Production URL https://warp-nexus.expo.app
```

Exit code 0. Production continued serving the prior build.

### Reproducible demo or steps to reproduce from a blank project

Each build stamps its entry-bundle content hash into `sw.js` as `const VERSION = ""`, which lets the live origin be asked which build it is actually serving. Any per-build marker in a static asset works for reproducing this — the point is to compare the deployment-specific URL against the production URL.

```bash
npx expo export --platform web
# note the hash of dist/_expo/static/js/web/entry-.js
npx eas-cli deploy --prod --export-dir dist --non-interactive
# command prints "Promoted deployment to production" and exits 0

curl -s "https://warp-nexus.expo.app/sw.js" | grep VERSION # OLD build
curl -s "https://warp-nexus--.expo.app/sw.js" | grep VERSION # NEW build
```

Measured immediately after a run that claimed to have promoted:

| Origin | `VERSION` served |
| --- | --- |
| `warp-nexus--y3iw49p3qz.expo.app` (that run's own deployment URL) | `7067f55ae830` ← the build just made |
| `warp-nexus.expo.app` (production) | `c0547b2ed28a` ← the **previous** build |

The new deployment exists and is complete at its own URL, so the export and upload are fine. Production was simply not moved to it.

Re-running the same command produced deployment `m9fb2fyn8e`; production then served `7067f55ae830`.

The same sequence repeated later that day with a different build: production stayed on `7067f55ae830` after a "successful" promotion, and moved to `c09a2c8b7e8e` only after a second identical run.

**Not a propagation delay.** Production is polled 6 times over 30 seconds before being called a failure, and stays on the old bundle throughout. After the second run it is correct within 10 seconds.

### Expected behaviour

Either the promotion happens, or the command reports that it did not and exits non-zero. A zero exit code printed alongside `✔ Promoted deployment to production` should not be compatible with production serving a different bundle.

### Impact

Without an external check, a deploy that appears to have succeeded leaves users on the old build indefinitely, with nothing anywhere to indicate it. We now gate every deploy on fetching `sw.js` from the production origin and comparing its hash against the local export, failing the deploy when they differ. That works, but it is a workaround for a command reporting an action it did not perform.

This is the same shape as #4185 (EAS Workflows reporting SKIPPED jobs as a `success` commit status): the failure mode is not that something broke, but that the tooling asserted success for work that did not happen.

Happy to supply project and deployment IDs privately if that helps trace the promotion calls server-side.

Contributor guide

Open the contributing guide

Research direction

Start at the eas-cli deploy flow invoked by `npx eas-cli deploy --prod --export-dir dist --non-interactive`, tracing the production promotion step after deployment creation. Use the deployment URL and production URL, polling `sw.js` and comparing its VERSION value to verify the result. Done means a failed promotion is detected and reported non-zero, or production serves the new deployment before success is printed.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, react-native, typescript
Domain
cli, cloud, release
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
63/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.