authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-02 17:24:01-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-02 17:24:01-07:00
log8ed276c246b624631e7d39241ebfd22d8314cb68
tree724f5102241fb66235a75f2c91c408d07aa763eb
parent641426885921b8d7caad1dbe35f804e6cec6a70b
signature Signed by PGP key B5690EEEBB952194

fix(mobile): production OTAs no longer reach the previous major's store binaries (#14944)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

3 files changed, 25 insertions(+), 3 deletions(-)

.github/workflows/mobile-eas-production.yml+5-2
......@@ -18,8 +18,11 @@ name: Mobile EAS Production
1818# Store stays a manual App Store Connect step.
1919# 2. OTA: publish a production-channel update for each platform where at
2020# least one finished production build matches the current native
21# fingerprint. Old-version binaries with a matching fingerprint receive
22# it too. When native drift means no binary could install the update,
21# fingerprint. Older binaries of the same major version with a matching
22# fingerprint receive it too; the major version is part of the
23# fingerprint (apps/mobile/fingerprint.config.js), so a new major never
24# reaches the previous major's store binaries over the air. When native
25# drift means no binary could install the update,
2326# it is skipped and flagged in the job summary instead of published
2427# into the void.
2528# workflow_dispatch remains as a manual override for both modes (e.g. to
apps/mobile/fingerprint.config.js created+18
......@@ -0,0 +1,18 @@
1// @ts-check
2const fs = require("node:fs");
3const path = require("node:path");
4
5// Expo's fingerprint ignores the app version, so binaries of different majors
6// share a runtime version whenever native code is unchanged, and a production
7// OTA from main would reach every older store binary. Hashing the major
8// version keeps each major's OTAs on its own binaries: a new major reaches
9// users only once its store build is promoted.
10const appConfig = fs.readFileSync(path.join(__dirname, "app.config.ts"), "utf8");
11const majorVersion = appConfig.match(/^ {2}version: "(\d+)\./m)?.[1];
12if (!majorVersion) {
13 throw new Error("fingerprint.config.js could not read the app version from app.config.ts");
14}
15
16module.exports = {
17 extraSources: [{ type: "contents", id: "appMajorVersion", contents: majorVersion }],
18};
knip.jsonc+2-1
......@@ -66,9 +66,10 @@
6666 "ignoreIssues": { "src/components/ui/*.tsx": ["exports", "nsExports", "duplicates"] },
6767 },
6868 "apps/mobile": {
69 // Expo loads plugins by string; Metro embeds the browser entry in the native WebView.
69 // Expo loads plugins and the fingerprint config by string; Metro embeds the browser entry in the native WebView.
7070 "entry": [
7171 "index.ts!",
72 "fingerprint.config.js",
7273 "plugins/*.cjs",
7374 "scripts/generate-device-stream.mts",
7475 "src/features/devices/device-stream.browser.ts!",