From 2d97d04805cbcf8ece396e0f3afaadd010e362cb Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 29 Oct 2026 14:39:42 +0700 Subject: [PATCH] Publish store screenshots from the repo on each stable release (#8186) * feat(release): publish store screenshots from the repo Stable releases now upload the committed store renders: the App Store review lane attaches the iPhone and iPad sets to the version it submits, and a new Play lane replaces the phone and tablet screenshots after the Android submit. F-Droid reads the same Android files, which now link to the store renders instead of the website images. The Play lane needs its own copy of the submit service-account key (GOOGLE_PLAY_JSON_KEY in the EAS production environment) because the key Expo stores for eas submit is not available to custom jobs, and EAS Metadata does not handle screenshots for either store. * fix(release): rewrite the Android screenshot links on every render Google Play and F-Droid read numbered links to the Google renders. The renderer now rewrites them from the panels it exported, so adding or renaming a panel reaches all three stores. (cherry picked from commit 7c88a48be47341f4dd70c155880f9351a9408330) --- assets/store-screenshots/README.md | 4 ++ assets/store-screenshots/render.mjs | 21 +++++++- docs/android.md | 5 +- docs/release.md | 15 +++++- .../en-US/images/phoneScreenshots/1.png | 2 +- .../en-US/images/phoneScreenshots/2.png | 2 +- .../en-US/images/phoneScreenshots/3.png | 2 +- .../en-US/images/phoneScreenshots/4.png | 1 + .../en-US/images/phoneScreenshots/5.png | 1 + .../en-US/images/tenInchScreenshots/1.png | 1 + .../en-US/images/tenInchScreenshots/2.png | 1 + .../en-US/images/tenInchScreenshots/3.png | 1 + .../en-US/images/tenInchScreenshots/4.png | 1 + .../en-US/images/tenInchScreenshots/5.png | 1 + .../app/.eas/workflows/release-mobile.yml | 14 ++++++ packages/app/fastlane/Fastfile | 49 ++++++++++++++++++- 16 files changed, 112 insertions(+), 9 deletions(-) create mode 120000 fastlane/metadata/android/en-US/images/phoneScreenshots/4.png create mode 120000 fastlane/metadata/android/en-US/images/phoneScreenshots/5.png create mode 120000 fastlane/metadata/android/en-US/images/tenInchScreenshots/1.png create mode 120000 fastlane/metadata/android/en-US/images/tenInchScreenshots/2.png create mode 120000 fastlane/metadata/android/en-US/images/tenInchScreenshots/3.png create mode 120000 fastlane/metadata/android/en-US/images/tenInchScreenshots/4.png create mode 120000 fastlane/metadata/android/en-US/images/tenInchScreenshots/5.png diff --git a/assets/store-screenshots/README.md b/assets/store-screenshots/README.md index 6db3d236ec..fc37d98bd8 100644 --- a/assets/store-screenshots/README.md +++ b/assets/store-screenshots/README.md @@ -95,4 +95,8 @@ Callout crops are in native capture pixels. To find them, overlay a 100 px grid 6. Copy the captures into `raw/iphone/` or `raw/ipad/` under the names `render.mjs` uses, render, and check the preview. +## Publish + +The stable release uploads these exports to the App Store and Google Play, and F-Droid reads the Google ones from the repo. See [Store screenshots](../../docs/release.md#store-screenshots). The Android metadata holds links to the Google exports, and every render rewrites them, so commit the links with the exports. + Dimensions are checked against [Apple's screenshot specifications](https://developer.apple.com/help/app-store-connect/reference/app-information/screenshot-specifications/) and [Google Play's preview asset requirements](https://support.google.com/googleplay/android-developer/answer/9866151?hl=en). diff --git a/assets/store-screenshots/render.mjs b/assets/store-screenshots/render.mjs index 9e32384a60..6695eac559 100644 --- a/assets/store-screenshots/render.mjs +++ b/assets/store-screenshots/render.mjs @@ -9,7 +9,7 @@ // elsewhere point SF_FONT_DIR at a folder holding SFNS.ttf copied from a Mac. import { existsSync } from "node:fs"; -import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; @@ -23,6 +23,7 @@ const root = path.dirname(fileURLToPath(import.meta.url)); const repo = path.resolve(root, "../.."); const raw = (device, name) => path.join(root, "raw", device, `${name}.png`); const output = path.join(root, "output"); +const androidImages = path.join(repo, "fastlane/metadata/android/en-US/images"); // Native captures per device: iPhone 16 Pro Max at 3x and iPad Pro 13-inch at // 2x. The frame's screen radius and Dynamic Island are in capture pixels. @@ -116,6 +117,7 @@ const STORES = { }, google: { device: "iphone", + androidScreenshots: "phoneScreenshots", width: 1440, height: 2560, textTop: 150, @@ -134,6 +136,7 @@ const STORES = { }, "google-tablet": { device: "ipad", + androidScreenshots: "tenInchScreenshots", width: 1800, height: 3200, textTop: 200, @@ -352,6 +355,18 @@ async function expectLines(view, selector, expected, label) { if (lines !== expected) throw new Error(`${label} wraps to ${lines} lines, expected ${expected}`); } +// Google Play and F-Droid read their screenshots from the Android metadata, +// which holds links to these renders. Rewriting the links on every render keeps +// added, removed and renamed panels in step with the App Store. +async function linkAndroidScreenshots(folder, files) { + const dir = path.join(androidImages, folder); + await rm(dir, { recursive: true, force: true }); + await mkdir(dir, { recursive: true }); + for (const [index, file] of files.entries()) { + await symlink(path.relative(dir, file), path.join(dir, `${index + 1}.png`)); + } +} + async function renderPreview(rows) { const tile = 440; const gap = 40; @@ -387,7 +402,9 @@ try { const marks = await loadAgentMarks(work); const rows = []; for (const [name, store] of Object.entries(STORES)) { - rows.push(await renderStore(browser, name, store, font, marks, work)); + const files = await renderStore(browser, name, store, font, marks, work); + if (store.androidScreenshots) await linkAndroidScreenshots(store.androidScreenshots, files); + rows.push(files); } await renderPreview(rows); } finally { diff --git a/docs/android.md b/docs/android.md index 546ba92a3e..6188589eb8 100644 --- a/docs/android.md +++ b/docs/android.md @@ -215,7 +215,8 @@ fastlane/metadata/android/ │ ├── images/ │ │ ├── icon.png (512x512) │ │ ├── featureGraphic.png (1024x500) -│ │ └── phoneScreenshots/ (1.png, 2.png, ...) +│ │ ├── phoneScreenshots/ (1.png, 2.png, ... linked to the store renders) +│ │ └── tenInchScreenshots/ (1.png, 2.png, ... linked to the store renders) │ └── changelogs/ (generated — see below) ├── ja/ └── zh-CN/ @@ -223,6 +224,8 @@ fastlane/metadata/android/ Locale directories generally match `packages/app/src/i18n/locales.ts`, but note that `en` becomes `en-US`. +Google Play reads its screenshots from the same folders; see [Store screenshots](release.md#store-screenshots). + F-Droid changelogs are generated from `CHANGELOG.md`. Run `npm run fdroid:changelogs`; `npm run fdroid:changelogs:check` verifies without writing. It is wired into the npm `version` lifecycle, so a release picks it up automatically and `git add -A` stages the result. One changelog must be generated per-ABI-split, so each version will create **four** identical version-coded entries. F-Droid caps changelogs at 500 characters, so the generator strips some content and adds a link to the full notes. diff --git a/docs/release.md b/docs/release.md index 83a419b8ce..ce0857abb3 100644 --- a/docs/release.md +++ b/docs/release.md @@ -305,8 +305,8 @@ macOS 13 maps to Darwin 22. The two values use different version domains; do not iOS and Android store builds are not in `.github/workflows`. They are triggered by the EAS GitHub app the moment the `v*` tag is pushed: -- **Android (Play Store)** — EAS builds with profile `production` and auto-submits to the Play Store via `eas submit` (EAS-managed credentials, no Fastlane). -- **iOS (TestFlight + App Store)** — EAS builds with profile `production`, uploads once through a `testflight` job, distributes the build to the `Paseo Beta` external group with Beta App Review submission, and a Fastlane lane submits that same build for App Store review. +- **Android (Play Store)** — EAS builds with profile `production` and auto-submits to the Play Store via `eas submit` (EAS-managed credentials). A Fastlane lane then replaces the store screenshots. +- **iOS (TestFlight + App Store)** — EAS builds with profile `production`, uploads once through a `testflight` job, distributes the build to the `Paseo Beta` external group with Beta App Review submission, and a Fastlane lane uploads the store screenshots and submits that same build for App Store review. - **Android APK (GitHub Release asset)** — separate, via `.github/workflows/android-apk-release.yml`. This is the only Android-related workflow that lives in this repo. EAS uses the local app version source. `packages/app/app.config.js` derives the native version from the package version. Android `versionCode` is `major * 1_000_000 + minor * 1_000 + patch`. iOS reserves 1,000 build slots per app version: beta `N` uses slot `N`, and stable uses slot `999`. For example, `0.2.6-beta.2` appears in App Store Connect as version `0.2.6` build `2006002`; stable uses build `2006999`. Rebuilding the same tag produces the same native build number; if a store has already accepted a binary and you need a different binary, cut the next beta or patch instead of relying on EAS remote auto-increment. @@ -317,6 +317,16 @@ Stable iOS releases use one binary for two Apple review paths: Beta App Review f There is no mobile-release workflow under `.github/workflows`. The EAS GitHub app reads the workflows under `packages/app/.eas/workflows` and handles tag triggering directly. +### Store screenshots + +Each stable release publishes the screenshots committed under `assets/store-screenshots/output/` (see [its README](../assets/store-screenshots/README.md) for how they are made). To change the store screenshots, re-render and commit them before you tag. + +- **App Store:** `submit_review` uploads `output/apple/` and `output/apple-ipad/` to the version it submits, replacing that version's `en-US` screenshots. Apple ties screenshots to an app version, so a version that is already live keeps the ones it shipped with. `Resubmit iOS for App Store review` uploads them again. +- **Google Play:** `upload_android_screenshots` runs `fastlane android upload_screenshots` after `submit_android`. It replaces only the phone and 10-inch tablet screenshots, never the listing text or changelogs, and skips images whose checksum already matches. +- **F-Droid:** reads the same files as Google Play. `fastlane/metadata/android/en-US/images/phoneScreenshots/` and `tenInchScreenshots/` hold symlinks to the Google renders. `render.mjs` rewrites them on every render, so added or renamed panels reach Google Play and F-Droid as well. + +Fastlane cannot use the Play key Expo stores for `eas submit`. The Play lane reads its own copy of the same service-account key from the `GOOGLE_PLAY_JSON_KEY` file secret in the EAS `production` environment, and that service account needs the Play Console permission **Manage store presence**. + ### Watching mobile builds from the terminal Use the EAS CLI from `packages/app/`: @@ -359,6 +369,7 @@ For the `Release Mobile` EAS workflow, these jobs must pass: - `submit_ios_for_review` — iOS build submitted for App Store review via Fastlane - `build_android` — Android store binary built - `submit_android` — Android binary submitted to the Play Store +- `upload_android_screenshots` — Google Play screenshots replaced from the repo Do not treat `build_ios: SUCCESS` or `distribute_ios: SUCCESS` as a completed iOS release. `submit_ios_for_review: FAILURE` means the iOS release is blocked even if the build is visible in TestFlight. diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png index 9f633ca58f..ca3a9fa2a8 120000 --- a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png +++ b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png @@ -1 +1 @@ -../../../../../../packages/website/public/phone-1.png \ No newline at end of file +../../../../../../assets/store-screenshots/output/google/01-sidebar.png \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png index 6d2fbd14d7..a0748cf426 120000 --- a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png +++ b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png @@ -1 +1 @@ -../../../../../../packages/website/public/phone-2.png \ No newline at end of file +../../../../../../assets/store-screenshots/output/google/02-chat.png \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png index e23c27ced5..0e7dba6b41 120000 --- a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png +++ b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png @@ -1 +1 @@ -../../../../../../packages/website/public/phone-3.png \ No newline at end of file +../../../../../../assets/store-screenshots/output/google/03-plugins.png \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png new file mode 120000 index 0000000000..e89becb9bc --- /dev/null +++ b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png @@ -0,0 +1 @@ +../../../../../../assets/store-screenshots/output/google/04-review.png \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png new file mode 120000 index 0000000000..c067990032 --- /dev/null +++ b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png @@ -0,0 +1 @@ +../../../../../../assets/store-screenshots/output/google/05-terminal.png \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/images/tenInchScreenshots/1.png b/fastlane/metadata/android/en-US/images/tenInchScreenshots/1.png new file mode 120000 index 0000000000..401053965e --- /dev/null +++ b/fastlane/metadata/android/en-US/images/tenInchScreenshots/1.png @@ -0,0 +1 @@ +../../../../../../assets/store-screenshots/output/google-tablet/01-sidebar.png \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/images/tenInchScreenshots/2.png b/fastlane/metadata/android/en-US/images/tenInchScreenshots/2.png new file mode 120000 index 0000000000..2454381850 --- /dev/null +++ b/fastlane/metadata/android/en-US/images/tenInchScreenshots/2.png @@ -0,0 +1 @@ +../../../../../../assets/store-screenshots/output/google-tablet/02-chat.png \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/images/tenInchScreenshots/3.png b/fastlane/metadata/android/en-US/images/tenInchScreenshots/3.png new file mode 120000 index 0000000000..1b31012051 --- /dev/null +++ b/fastlane/metadata/android/en-US/images/tenInchScreenshots/3.png @@ -0,0 +1 @@ +../../../../../../assets/store-screenshots/output/google-tablet/03-plugins.png \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/images/tenInchScreenshots/4.png b/fastlane/metadata/android/en-US/images/tenInchScreenshots/4.png new file mode 120000 index 0000000000..13d8545d92 --- /dev/null +++ b/fastlane/metadata/android/en-US/images/tenInchScreenshots/4.png @@ -0,0 +1 @@ +../../../../../../assets/store-screenshots/output/google-tablet/04-review.png \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/images/tenInchScreenshots/5.png b/fastlane/metadata/android/en-US/images/tenInchScreenshots/5.png new file mode 120000 index 0000000000..b0905beafc --- /dev/null +++ b/fastlane/metadata/android/en-US/images/tenInchScreenshots/5.png @@ -0,0 +1 @@ +../../../../../../assets/store-screenshots/output/google-tablet/05-terminal.png \ No newline at end of file diff --git a/packages/app/.eas/workflows/release-mobile.yml b/packages/app/.eas/workflows/release-mobile.yml index 26790ef22f..e57260898c 100644 --- a/packages/app/.eas/workflows/release-mobile.yml +++ b/packages/app/.eas/workflows/release-mobile.yml @@ -42,6 +42,20 @@ jobs: build_id: ${{ needs.build_android.outputs.build_id }} profile: production + upload_android_screenshots: + name: Upload Google Play screenshots + needs: [build_android, submit_android] + environment: production + runs_on: macos-medium + steps: + - uses: eas/checkout + - name: Install fastlane + run: bundle install + - name: Upload screenshots + run: | + export ANDROID_VERSION_CODE="${{ needs.build_android.outputs.app_build_version }}" + bundle exec fastlane android upload_screenshots + submit_ios_for_review: name: Submit iOS for App Store review needs: [build_ios, distribute_ios] diff --git a/packages/app/fastlane/Fastfile b/packages/app/fastlane/Fastfile index dc7508b456..065ca48b7f 100644 --- a/packages/app/fastlane/Fastfile +++ b/packages/app/fastlane/Fastfile @@ -1,7 +1,31 @@ +require "fileutils" +require "tmpdir" + default_platform(:ios) APP_IDENTIFIER = "sh.paseo" +# Store screenshots are rendered into assets/store-screenshots/output; see the +# README there. Google Play and F-Droid both read fastlane/metadata/android at +# the repo root, whose screenshot files link to those renders. +REPO_ROOT = File.expand_path("../../..", __dir__) +STORE_SCREENSHOTS = File.join(REPO_ROOT, "assets/store-screenshots/output") +ANDROID_METADATA = File.join(REPO_ROOT, "fastlane/metadata/android") + +# deliver reads screenshots from one folder per language and picks the device +# slot from each image's size, so both Apple sets go into en-US together. +def stage_app_store_screenshots + root = Dir.mktmpdir("app-store-screenshots") + locale = File.join(root, "en-US") + FileUtils.mkdir_p(locale) + { "apple" => "iphone", "apple-ipad" => "ipad" }.each do |set, device| + Dir.glob(File.join(STORE_SCREENSHOTS, set, "*.png")).sort.each do |file| + FileUtils.cp(file, File.join(locale, "#{device}-#{File.basename(file)}")) + end + end + root +end + platform :ios do desc "Submit the latest TestFlight build to App Store Review" lane :submit_review do @@ -87,7 +111,8 @@ platform :ios do }, force: true, skip_binary_upload: true, - skip_screenshots: true, + screenshots_path: stage_app_store_screenshots, + overwrite_screenshots: true, precheck_include_in_app_purchases: false, submission_information: { add_id_info_uses_idfa: false, @@ -96,3 +121,25 @@ platform :ios do ) end end + +platform :android do + desc "Replace the Google Play store screenshots with the ones in the repo" + lane :upload_screenshots do + # supply attaches listing changes to a release on the production track. + # The release workflow passes the version code it just submitted; without + # it, supply uses the track's only release. + version_code = ENV["ANDROID_VERSION_CODE"].to_s.strip + supply( + package_name: APP_IDENTIFIER, + json_key: ENV.fetch("GOOGLE_PLAY_JSON_KEY"), + metadata_path: ANDROID_METADATA, + version_code: version_code.empty? ? nil : version_code.to_i, + skip_upload_apk: true, + skip_upload_aab: true, + skip_upload_metadata: true, + skip_upload_changelogs: true, + skip_upload_images: true, + sync_image_upload: true, + ) + end +end