| author | |
| committer | |
| log | 8ed276c246b624631e7d39241ebfd22d8314cb68 |
| tree | 724f5102241fb66235a75f2c91c408d07aa763eb |
| parent | 641426885921b8d7caad1dbe35f804e6cec6a70b |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | ... | @@ -18,8 +18,11 @@ name: Mobile EAS Production |
| 18 | # Store stays a manual App Store Connect step. | 18 | # Store stays a manual App Store Connect step. |
| 19 | # 2. OTA: publish a production-channel update for each platform where at | 19 | # 2. OTA: publish a production-channel update for each platform where at |
| 20 | # least one finished production build matches the current native | 20 | # least one finished production build matches the current native |
| 21 | # fingerprint. Old-version binaries with a matching fingerprint receive | 21 | # fingerprint. Older binaries of the same major version with a matching |
| 22 | # it too. When native drift means no binary could install the update, | 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, | ||
| 23 | # it is skipped and flagged in the job summary instead of published | 26 | # it is skipped and flagged in the job summary instead of published |
| 24 | # into the void. | 27 | # into the void. |
| 25 | # workflow_dispatch remains as a manual override for both modes (e.g. to | 28 | # 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 | ||
| 2 | const fs = require("node:fs"); | ||
| 3 | const 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. | ||
| 10 | const appConfig = fs.readFileSync(path.join(__dirname, "app.config.ts"), "utf8"); | ||
| 11 | const majorVersion = appConfig.match(/^ {2}version: "(\d+)\./m)?.[1]; | ||
| 12 | if (!majorVersion) { | ||
| 13 | throw new Error("fingerprint.config.js could not read the app version from app.config.ts"); | ||
| 14 | } | ||
| 15 | |||
| 16 | module.exports = { | ||
| 17 | extraSources: [{ type: "contents", id: "appMajorVersion", contents: majorVersion }], | ||
| 18 | }; | ||
knip.jsonc+2-1| ... | @@ -66,9 +66,10 @@ | ... | @@ -66,9 +66,10 @@ |
| 66 | "ignoreIssues": { "src/components/ui/*.tsx": ["exports", "nsExports", "duplicates"] }, | 66 | "ignoreIssues": { "src/components/ui/*.tsx": ["exports", "nsExports", "duplicates"] }, |
| 67 | }, | 67 | }, |
| 68 | "apps/mobile": { | 68 | "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. |
| 70 | "entry": [ | 70 | "entry": [ |
| 71 | "index.ts!", | 71 | "index.ts!", |
| 72 | "fingerprint.config.js", | ||
| 72 | "plugins/*.cjs", | 73 | "plugins/*.cjs", |
| 73 | "scripts/generate-device-stream.mts", | 74 | "scripts/generate-device-stream.mts", |
| 74 | "src/features/devices/device-stream.browser.ts!", | 75 | "src/features/devices/device-stream.browser.ts!", |