diff --git a/apps/readest-app/.claude/memory/fastlane-apple-appstore-submission.md b/apps/readest-app/.claude/memory/fastlane-apple-appstore-submission.md new file mode 100644 index 00000000..5ec87253 --- /dev/null +++ b/apps/readest-app/.claude/memory/fastlane-apple-appstore-submission.md @@ -0,0 +1,18 @@ +--- +name: fastlane-apple-appstore-submission +description: "fastlane lanes for iOS/macOS App Store + TestFlight submission, and two gotchas (Tauri notarization trigger, fastlane cwd)" +metadata: + node_type: memory + type: project + originSessionId: 6604c57a-dee4-4a6e-8624-540162f41a80 +--- + +Readest's Apple App Store + TestFlight submission via fastlane (root `fastlane/Fastfile`, alongside the existing Android `upload_to_play_store` lanes). Builds are unchanged (`pnpm run release-ios-appstore` / `release-macos-universial-appstore` → `tauri build` + `xcrun altool --upload-app`); fastlane only does the post-upload App Store version + review submission and TestFlight distribution on the already-uploaded build. + +Lanes (per-platform, each does App Store review submit AND TestFlight, sharing a `submit_apple_build` helper): `release_ios`, `release_macos`. App Store via `upload_to_app_store(skip_binary_upload: true, ipa:/pkg:, platform: "ios"/"osx", submit_for_review: true, automatic_release: true, force: true, skip_screenshots: true, skip_metadata: false, release_notes:{"en-US"=>...}, promotional_text:{"en-US"=>...})`; TestFlight via `upload_to_testflight(distribute_only: true, app_platform: "ios"/"osx", distribute_external: true, groups:["Beta Testers"])`. App Store submit runs FIRST (it waits for build processing, which the TestFlight distribute then needs). `release_notes_text` parses `apps/readest-app/release-notes.json` (latest version by `Gem::Version`, drops notes matching `/\b(?:Android|Windows|Linux)\b/i`, prefixes each `– `). Auth: `app_store_connect_api_key`. Commands: `pnpm run submit-appstore-ios` / `submit-appstore-macos`. + +GOTCHA 1 (Tauri notarization): `tauri build` auto-notarizes the macOS App Store bundle whenever the FULL App Store Connect API key trio (`APPLE_API_KEY` + `APPLE_API_ISSUER` + `APPLE_API_KEY_PATH`) is in the build env. Notarization REJECTS App Store builds ("not signed with a valid Developer ID certificate" / "no secure timestamp") because they use an Apple Distribution cert — App Store apps are NOT notarized. So `APPLE_API_KEY_PATH` must stay OUT of `.env.apple-appstore.local` (the macOS build env). `asc_api_key` instead DERIVES the `.p8` path from the key id: `repo_path("apps/readest-app/private_keys/AuthKey_#{key_id}.p8")` (the keys are named `AuthKey_.p8`, same convention altool uses; honors an explicit `APPLE_API_KEY_PATH` when set, e.g. the iOS build env which DOES need it and iOS doesn't notarize). + +GOTCHA 2 (fastlane cwd): fastlane changes cwd to the `./fastlane` folder when EXECUTING a lane (`__dir__` is just "."), so raw `File.read("./apps/...")` breaks with "No such file". `fastlane lanes` only PARSES (doesn't run lane bodies) so it won't catch this — verify path-dependent lanes by actually RUNNING one. Fix = `repo_path(rel) = File.expand_path(rel, File.expand_path("..", __dir__))`, route every path (release-notes.json, .p8, ipa, pkg) through it. + +GOTCHA 3 (dotenv shadowing): bare `dotenv` on PATH is the Ruby gem (`-f` syntax); package.json scripts use the npm `dotenv-cli` (`-e` syntax) resolved from `apps/readest-app/node_modules/.bin`. The submit scripts run `dotenv -e .env.apple-appstore.local -- bash -c 'cd ../.. && fastlane release_*'` — the `cd ../..` is required because fastlane does NOT search upward for the `fastlane/` dir (pnpm runs scripts from `apps/readest-app`). diff --git a/apps/readest-app/package.json b/apps/readest-app/package.json index 13aab411..7236109a 100644 --- a/apps/readest-app/package.json +++ b/apps/readest-app/package.json @@ -71,6 +71,8 @@ "build-ios-appstore": "dotenv -e .env.ios-appstore.local -- tauri ios build --export-method app-store-connect", "release-macos-universial-appstore": "dotenv -e .env.tauri.local -e .env.apple-appstore.local -- bash scripts/release-mac-appstore.sh", "release-ios-appstore": "dotenv -e .env.ios-appstore.local -- bash scripts/release-ios-appstore.sh", + "submit-appstore-ios": "dotenv -e .env.apple-appstore.local -- bash -c 'cd ../.. && fastlane release_ios'", + "submit-appstore-macos": "dotenv -e .env.apple-appstore.local -- bash -c 'cd ../.. && fastlane release_macos'", "release-google-play": "dotenv -e .env.google-play.local -- bash scripts/release-google-play.sh", "config-wrangler": "sed -i \"s/\\${TRANSLATIONS_KV_ID}/$TRANSLATIONS_KV_ID/g\" wrangler.toml", "preview": "pnpm patch-build-webpack && NEXT_PUBLIC_APP_PLATFORM=web opennextjs-cloudflare build && pnpm restore-build-original && opennextjs-cloudflare preview --ip 0.0.0.0 --port 3001", diff --git a/apps/readest-app/scripts/release-ios-appstore.sh b/apps/readest-app/scripts/release-ios-appstore.sh index 23a1baf7..2b28e0a3 100644 --- a/apps/readest-app/scripts/release-ios-appstore.sh +++ b/apps/readest-app/scripts/release-ios-appstore.sh @@ -4,3 +4,7 @@ BUNDLE_DIR=src-tauri/gen/apple/build/arm64 IPA_BUNDLE=$BUNDLE_DIR/Readest.ipa xcrun altool --upload-app --type ios --file $IPA_BUNDLE --apiKey $APPLE_API_KEY --apiIssuer $APPLE_API_ISSUER + +echo "iOS build uploaded to App Store Connect." +echo "Submit it to App Store + TestFlight with:" +echo " pnpm run submit-appstore-ios" diff --git a/apps/readest-app/scripts/release-mac-appstore.sh b/apps/readest-app/scripts/release-mac-appstore.sh index e386a168..cc78a1ef 100644 --- a/apps/readest-app/scripts/release-mac-appstore.sh +++ b/apps/readest-app/scripts/release-mac-appstore.sh @@ -24,4 +24,8 @@ APP_BUNDLE=$BUNDLE_DIR/Readest.app INSTALLER_BUNDLE=$BUNDLE_DIR/Readest.pkg xcrun productbuild --sign "$APPLE_INSTALLER_SIGNING_IDENTITY" --component $APP_BUNDLE /Applications $INSTALLER_BUNDLE -xcrun altool --upload-app --type macos --file $INSTALLER_BUNDLE --apiKey $APPLE_API_KEY --apiIssuer $APPLE_API_ISSUER \ No newline at end of file +xcrun altool --upload-app --type macos --file $INSTALLER_BUNDLE --apiKey $APPLE_API_KEY --apiIssuer $APPLE_API_ISSUER + +echo "macOS build uploaded to App Store Connect." +echo "Submit it to App Store + TestFlight with:" +echo " pnpm run submit-appstore-macos" \ No newline at end of file diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 1f95a149..4cf9587d 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1,3 +1,5 @@ +require "json" + default_platform(:android) platform :android do @@ -42,3 +44,89 @@ platform :android do ) end end + +# fastlane executes lanes from the ./fastlane folder, so paths are anchored to the +# repo root (its parent) to stay independent of the caller's working directory. +def repo_path(relative) + File.expand_path(relative, File.expand_path("..", __dir__)) +end + +def asc_api_key + key_id = ENV["APPLE_API_KEY"] + # APPLE_API_KEY_PATH is deliberately kept OUT of the App Store build env: with the + # full key_id/issuer/path trio present, `tauri build` notarizes the App Store binary, + # which fails (App Store apps use an Apple Distribution cert, not Developer ID). + # Derive the .p8 path from the key id (same convention altool uses); honor an + # explicit APPLE_API_KEY_PATH when set (e.g. the iOS build env). + app_store_connect_api_key( + key_id: key_id, + issuer_id: ENV["APPLE_API_ISSUER"], + key_filepath: ENV["APPLE_API_KEY_PATH"] || repo_path("apps/readest-app/private_keys/AuthKey_#{key_id}.p8"), + ) +end + +# Release notes from the latest version in release-notes.json, excluding notes +# that mention Android, Windows, or Linux. Used for both the TestFlight changelog +# and the App Store "What's New" text. +def release_notes_text + releases = JSON.parse(File.read(repo_path("apps/readest-app/release-notes.json")))["releases"] + latest = releases.keys.max_by { |version| Gem::Version.new(version) } + releases[latest]["notes"] + .reject { |note| note =~ /\b(?:Android|Windows|Linux)\b/i } + .map { |note| "– #{note}" } + .join("\n") +end + +# App Store "Promotional Text". All other metadata fields keep their +# existing App Store Connect values. +PROMOTIONAL_TEXT = "Experience the Ultimate Ebook Reader Across All Your Devices for Avid Readers" + +# iOS and macOS are one App Store Connect app (com.bilingify.readest), built from +# separate binaries and submitted independently. Each lane acts only on a build +# already uploaded by altool (skip_binary_upload / distribute_only) — it never +# builds or uploads a binary. For its platform it submits the build for App Store +# review AND distributes it to TestFlight. The App Store submit runs first because +# it waits for build processing, which the TestFlight distribute then relies on. +# pnpm run submit-appstore-ios # iOS +# pnpm run submit-appstore-macos # macOS +def submit_apple_build(deliver_opts, testflight_opts) + api_key = asc_api_key + upload_to_app_store({ + api_key: api_key, + app_identifier: "com.bilingify.readest", + skip_binary_upload: true, # use the altool-uploaded build; read version from the binary + submit_for_review: true, + automatic_release: true, + force: true, # skip the HTML metadata preview prompt + skip_screenshots: true, + skip_metadata: false, # upload What's New + Promotional Text; other fields stay as-is + release_notes: { "en-US" => release_notes_text }, + promotional_text: { "en-US" => PROMOTIONAL_TEXT }, + precheck_include_in_app_purchases: false, + submission_information: { add_id_info_uses_idfa: false }, + }.merge(deliver_opts)) + upload_to_testflight({ + api_key: api_key, + app_identifier: "com.bilingify.readest", + distribute_only: true, # use the already-uploaded build; do not upload + distribute_external: true, + groups: ["Beta Testers"], + changelog: release_notes_text, + }.merge(testflight_opts)) +end + +desc "Submit the uploaded iOS build for App Store review and to TestFlight" +lane :release_ios do + submit_apple_build( + { platform: "ios", ipa: repo_path("apps/readest-app/src-tauri/gen/apple/build/arm64/Readest.ipa") }, + { app_platform: "ios" } + ) +end + +desc "Submit the uploaded macOS build for App Store review and to TestFlight" +lane :release_macos do + submit_apple_build( + { platform: "osx", pkg: repo_path("target/universal-apple-darwin/release/bundle/macos/Readest.pkg") }, + { app_platform: "osx" } + ) +end diff --git a/fastlane/README.md b/fastlane/README.md index f95525b8..e247113a 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -13,6 +13,33 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do # Available Actions +### verify_paths + +```sh +[bundle exec] fastlane verify_paths +``` + + + +### release_ios + +```sh +[bundle exec] fastlane release_ios +``` + +Submit the uploaded iOS build for App Store review and to TestFlight + +### release_macos + +```sh +[bundle exec] fastlane release_macos +``` + +Submit the uploaded macOS build for App Store review and to TestFlight + +---- + + ## Android ### android upload_production