# This file contains the fastlane.tools configuration # You can find the documentation at https://docs.fastlane.tools # # For a list of all available actions, check out # # https://docs.fastlane.tools/actions # # For a list of all available plugins, check out # # https://docs.fastlane.tools/plugins/available-plugins # # Uncomment the line if you want fastlane to automatically update itself # update_fastlane default_platform(:ios) platform :ios do desc "Push a new beta build to TestFlight" lane :beta do UI.message "Current working directory: #{Dir.pwd}" UI.message "Checking API key file..." possible_paths = [ "../AuthKey.p8", "./AuthKey.p8", "../../AuthKey.p8", File.expand_path("../AuthKey.p8"), File.expand_path("./AuthKey.p8") ] auth_key_path = nil possible_paths.each do |path| if File.exist?(path) UI.message "Found AuthKey.p8 at: #{path}" auth_key_path = File.expand_path(path) break end end if auth_key_path.nil? UI.user_error!("API key file not found. Tried paths: #{possible_paths.join(', ')}") 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 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| 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_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 version from pubspec: #{flutter_build_name} (#{flutter_build_number})" # Apple closes a version train once that version is approved/released. # Uploading into a closed train wastes a full ~8 min build and fails only # at the upload step with altool 90186 ("closed for new build submissions") # / 90062 ("must be higher than the previously approved version"). # Fail fast here, before building. closed_train_states = [ Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::READY_FOR_DISTRIBUTION, Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::PROCESSING_FOR_DISTRIBUTION, Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::PENDING_DEVELOPER_RELEASE, Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::PENDING_APPLE_RELEASE ].join(",") asc_app = Spaceship::ConnectAPI::App.find("com.eatvioapp.app") closed_train = nil if asc_app.nil? UI.important("App com.eatvioapp.app not found in App Store Connect, skipping closed-train guard") else closed_train = asc_app.get_app_store_versions( filter: { appVersionState: closed_train_states, platform: Spaceship::ConnectAPI::Platform::IOS } ).find do |v| (Gem::Version.new(v.version_string) >= Gem::Version.new(flutter_build_name)) rescue ArgumentError false end end if closed_train UI.user_error!( "Version train #{closed_train.version_string} is closed for new submissions " \ "(#{closed_train.app_version_state}), pubspec version is #{flutter_build_name}. " \ "Bump the version in pubspec.yaml above #{closed_train.version_string} and re-run." ) end 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: new_build_number ) update_code_signing_settings( use_automatic_signing: false, path: "Runner.xcodeproj", team_id: "VL68C36HYS", targets: ["Runner"], code_sign_identity: "Apple Distribution", profile_name: "match AppStore com.eatvioapp.app 1781098260" ) build_app( workspace: "Runner.xcworkspace", scheme: "Runner", export_method: "app-store", # codesigning_identity: "Apple Distribution", export_team_id: "VL68C36HYS" ) begin upload_to_testflight( skip_waiting_for_build_processing: true, skip_submission: true, distribute_external: false, notify_external_testers: false, changelog: "TestFlight build #{new_build_number}", verbose: true ) rescue => ex # Xcode 26 altool prints a harmless "ERROR: ... CDUploaderStateInfoResumeFilePath" # line even when the package was delivered fine (UPLOAD SUCCEEDED appears in the # same output). fastlane < 2.240.0 treats every ERROR line as a failure # (fastlane#30130), so the lane would die after a successful upload. # Real Apple rejections (90186 closed train, 90062 version too low, ...) always # contain UPLOAD FAILED and/or iris-code - those must still fail the lane. if ex.message.include?("CDUploaderStateInfoResumeFilePath") && !ex.message.include?("UPLOAD FAILED") && !ex.message.include?("iris-code") UI.important("altool delivered the package but reported a bogus resume-file ERROR " \ "(fastlane#30130). Treating upload as successful - " \ "verify the build appears in TestFlight.") else UI.error "Upload to TestFlight failed: #{ex.message}" UI.error "Full error: #{ex.backtrace.join("\n")}" raise ex end end end end