From f4a4e0c25c98748088eb8f7b857bd6701483d7b2 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sat, 16 May 2026 16:43:36 +0700 Subject: [PATCH] fix(ios-release): submit each iOS build to its own App Store version The submit_review fastlane lane identified the just-uploaded build by CFBundleVersion alone. In this project the production iOS profile resets the build number per marketing version, so every release sits at build 2 and Spaceship::ConnectAPI::Build.all returns the whole TestFlight history. The .find call picked an arbitrary match, which is how 0.1.76's binary got attached to the 0.1.75 App Store version slot and submitted for review under the wrong label. The release workflow now forwards app_version and app_build_version from the build_ios outputs into the fastlane step as env vars, and the Fastfile filters builds by marketing version plus build number to pin the exact binary. The standalone resubmit workflow takes the same identifiers as optional workflow_dispatch inputs; with both unset it falls back to "most recently uploaded iOS build" so manual reruns still work. Pinning fastlane in the Gemfile to lock the Spaceship behavior this lane depends on. --- .../app/.eas/workflows/release-mobile.yml | 7 ++- .../.eas/workflows/resubmit-ios-review.yml | 16 ++++++- packages/app/Gemfile | 2 +- packages/app/fastlane/Fastfile | 47 ++++++++++++++----- 4 files changed, 55 insertions(+), 17 deletions(-) diff --git a/packages/app/.eas/workflows/release-mobile.yml b/packages/app/.eas/workflows/release-mobile.yml index 33f1c2ea6..13968b8b5 100644 --- a/packages/app/.eas/workflows/release-mobile.yml +++ b/packages/app/.eas/workflows/release-mobile.yml @@ -41,7 +41,7 @@ jobs: submit_ios_for_review: name: Submit iOS for App Store review - needs: [submit_ios] + needs: [build_ios, submit_ios] environment: production runs_on: macos-medium steps: @@ -49,4 +49,7 @@ jobs: - name: Install fastlane run: bundle install - name: Submit for review - run: bundle exec fastlane ios submit_review + run: | + export APP_VERSION="${{ needs.build_ios.outputs.app_version }}" + export APP_BUILD_VERSION="${{ needs.build_ios.outputs.app_build_version }}" + bundle exec fastlane ios submit_review diff --git a/packages/app/.eas/workflows/resubmit-ios-review.yml b/packages/app/.eas/workflows/resubmit-ios-review.yml index 0b203de2c..906f8b124 100644 --- a/packages/app/.eas/workflows/resubmit-ios-review.yml +++ b/packages/app/.eas/workflows/resubmit-ios-review.yml @@ -6,7 +6,16 @@ name: Resubmit iOS for App Store review # TestFlight build, without rebuilding or re-uploading. on: - workflow_dispatch: {} + workflow_dispatch: + inputs: + app_version: + type: string + required: false + description: "Marketing version to resubmit, e.g. 0.1.76. Leave empty to target the most recently uploaded iOS build." + app_build_version: + type: string + required: false + description: "CFBundleVersion to resubmit, e.g. 2. Only used when app_version is set." jobs: submit_ios_for_review: @@ -18,4 +27,7 @@ jobs: - name: Install fastlane run: bundle install - name: Submit for review - run: bundle exec fastlane ios submit_review + run: | + export APP_VERSION="${{ inputs.app_version }}" + export APP_BUILD_VERSION="${{ inputs.app_build_version }}" + bundle exec fastlane ios submit_review diff --git a/packages/app/Gemfile b/packages/app/Gemfile index 7a118b49b..102550f22 100644 --- a/packages/app/Gemfile +++ b/packages/app/Gemfile @@ -1,3 +1,3 @@ source "https://rubygems.org" -gem "fastlane" +gem "fastlane", "~> 2.234" diff --git a/packages/app/fastlane/Fastfile b/packages/app/fastlane/Fastfile index 2af230c85..e8d28503d 100644 --- a/packages/app/fastlane/Fastfile +++ b/packages/app/fastlane/Fastfile @@ -11,11 +11,6 @@ platform :ios do key_filepath: ENV.fetch("ASC_KEY_P8"), ) - build_number = latest_testflight_build_number( - api_key: api_key, - app_identifier: APP_IDENTIFIER, - ) - require "spaceship" Spaceship::ConnectAPI.token = Spaceship::ConnectAPI::Token.create( key_id: ENV.fetch("ASC_KEY_ID"), @@ -25,30 +20,58 @@ platform :ios do app = Spaceship::ConnectAPI::App.find(APP_IDENTIFIER) UI.user_error!("Could not find app #{APP_IDENTIFIER} on App Store Connect") if app.nil? + # The release workflow forwards the marketing version and CFBundleVersion of + # the binary it just built. Use them to identify the exact build, since + # CFBundleVersion is not unique across marketing versions in this project + # (the production profile resets it per app_version). The resubmit workflow + # leaves these unset and falls back to "most recently uploaded iOS build". + target_version = ENV["APP_VERSION"].to_s.strip + target_build = ENV["APP_BUILD_VERSION"].to_s.strip + target_version = nil if target_version.empty? + target_build = nil if target_build.empty? + + if target_version + UI.message("Targeting App Store version #{target_version}#{target_build ? " build #{target_build}" : ""} from workflow input") + else + UI.message("No APP_VERSION provided; selecting the most recently uploaded iOS build") + end + valid_build = nil deadline = Time.now + (30 * 60) loop do builds = Spaceship::ConnectAPI::Build.all( app_id: app.id, - build_number: build_number.to_s, - ) - build = builds.find { |b| b.pre_release_version&.platform == "IOS" } - state = build&.processing_state - UI.message("Build #{build_number} processing state: #{state || 'unknown'}") + sort: "-uploadedDate", + limit: 100, + ).select { |b| b.pre_release_version&.platform == "IOS" } + builds = builds.select { |b| b.pre_release_version&.version == target_version } if target_version + builds = builds.select { |b| b.version == target_build } if target_build + + build = builds.first + if build.nil? + UI.user_error!("Timed out waiting for build to appear on App Store Connect.") if Time.now > deadline + UI.message("No matching iOS build visible on App Store Connect yet; waiting...") + sleep(30) + next + end + + state = build.processing_state + UI.message("Build #{build.pre_release_version.version} (#{build.version}) processing state: #{state || 'unknown'}") if state == "VALID" valid_build = build break end if state == "INVALID" || state == "FAILED" - UI.user_error!("Build #{build_number} failed App Store processing (state: #{state}).") + UI.user_error!("Build #{build.version} failed App Store processing (state: #{state}).") end if Time.now > deadline - UI.user_error!("Timed out waiting for build #{build_number} to finish processing on App Store Connect.") + UI.user_error!("Timed out waiting for build #{build.version} to finish processing on App Store Connect.") end sleep(30) end app_version = valid_build.pre_release_version.version + build_number = valid_build.version UI.message("Submitting build #{build_number} as App Store version #{app_version} for review") # Ensure an editable App Store version exists for this marketing version.