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 = "<hash>", 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.
npx expo export --platform web
# note the hash of dist/_expo/static/js/web/entry-<hash>.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--<deployId>.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.
Summary
eas deploy --prodprints✔ Promoted deployment to productionand 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
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:
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.jsasconst VERSION = "<hash>", 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.Measured immediately after a run that claimed to have promoted:
VERSIONservedwarp-nexus--y3iw49p3qz.expo.app(that run's own deployment URL)7067f55ae830← the build just madewarp-nexus.expo.app(production)c0547b2ed28a← the previous buildThe 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 served7067f55ae830.The same sequence repeated later that day with a different build: production stayed on
7067f55ae830after a "successful" promotion, and moved toc09a2c8b7e8eonly 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 productionshould 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.jsfrom 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
successcommit 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.