From f6f97648115336ce388a0cb3b45afc3b3fec96fc Mon Sep 17 00:00:00 2001 From: Anatoliy Date: Wed, 28 Oct 2026 11:10:22 +0300 Subject: [PATCH] fix(ios): derive TestFlight build number from App Store Connect The beta lane took the build number from pubspec (FLUTTER_BUILD_NUMBER). Re-running CI on the same commit re-submitted an already-taken build number and failed with 409 ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE (run 2045: build 157 was already on TestFlight). Now the lane queries the latest TestFlight build for the current marketing version (falling back to the pubspec value when nothing was uploaded yet) and uploads latest + 1, so retries can never collide. --- ios/fastlane/Fastfile | 64 ++++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 25 deletions(-) diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 904991ee..64898665 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -43,37 +43,60 @@ platform :ios do end UI.message "Using auth_key_path: #{auth_key_path}" - + + # Authenticate to App Store Connect early: + # needed here by latest_testflight_build_number, not only by upload_to_testflight. + app_store_connect_api_key( + key_id: "325JS9JCZH", + issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", + key_filepath: auth_key_path, + duration: 1200, + in_house: false + ) + # Configure Flutter for release mode UI.message "Configuring Flutter for release mode..." sh("cd .. && flutter build ios --config-only --release") - # Read build number from Flutter Generated.xcconfig + # Read version and build number from Flutter Generated.xcconfig. + # They are only a floor: the actual build number is derived from TestFlight below, + # so repeated CI runs on the same commit can never hit + # ENTITY_ERROR.ATTRIBUTE.INVALID.DUPLICATE ("bundle version must be higher"). generated_xcconfig_path = File.join("..", "Flutter", "Generated.xcconfig") + flutter_build_name = nil flutter_build_number = nil - + if File.exist?(generated_xcconfig_path) File.foreach(generated_xcconfig_path) do |line| - matches = line.match(/FLUTTER_BUILD_NUMBER=(.*)/) - if matches - flutter_build_number = matches[1].strip - break + if (m = line.match(/FLUTTER_BUILD_NAME=(.*)/)) + flutter_build_name = m[1].strip + elsif (m = line.match(/FLUTTER_BUILD_NUMBER=(.*)/)) + flutter_build_number = m[1].strip end end end - - if flutter_build_number.nil? - UI.user_error!("Could not find FLUTTER_BUILD_NUMBER in #{generated_xcconfig_path}") + + if flutter_build_name.nil? || flutter_build_number.nil? + UI.user_error!("Could not find FLUTTER_BUILD_NAME/FLUTTER_BUILD_NUMBER in #{generated_xcconfig_path}") end - - UI.message "Flutter build number: #{flutter_build_number}" - + + UI.message "Flutter version from pubspec: #{flutter_build_name} (#{flutter_build_number})" + + asc_latest_build_number = latest_testflight_build_number( + app_identifier: "com.eatvioapp.app", + version: flutter_build_name, + initial_build_number: flutter_build_number.to_i + ) + + new_build_number = [flutter_build_number.to_i, asc_latest_build_number].max + 1 + UI.message "Latest TestFlight build number: #{asc_latest_build_number} -> uploading build #{new_build_number}" + match(type: "appstore", readonly: true) - + # Set build number explicitly instead of incrementing increment_build_number( xcodeproj: "Runner.xcodeproj", - build_number: flutter_build_number + build_number: new_build_number ) update_code_signing_settings( @@ -93,22 +116,13 @@ platform :ios do export_team_id: "VL68C36HYS" ) - app_store_connect_api_key( - key_id: "325JS9JCZH", - issuer_id: "dfa27d1d-2bb8-4d4d-847e-0b32f9b880b3", - key_filepath: auth_key_path, - duration: 1200, - in_house: false - ) - - begin upload_to_testflight( skip_waiting_for_build_processing: true, skip_submission: true, distribute_external: false, notify_external_testers: false, - changelog: "TestFlight build #{flutter_build_number}", + changelog: "TestFlight build #{new_build_number}", verbose: true ) rescue => ex