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