diff --git a/fastlane/lib/fastlane/actions/upload_to_testflight.rb b/fastlane/lib/fastlane/actions/upload_to_testflight.rb index a6aa33317fe..5f24750692d 100644 --- a/fastlane/lib/fastlane/actions/upload_to_testflight.rb +++ b/fastlane/lib/fastlane/actions/upload_to_testflight.rb @@ -13,6 +13,8 @@ def self.run(values) unless distribute_only values[:ipa] ||= Actions.lane_context[SharedValues::IPA_OUTPUT_PATH] values[:ipa] = File.expand_path(values[:ipa]) if values[:ipa] + values[:pkg] ||= Actions.lane_context[SharedValues::PKG_OUTPUT_PATH] + values[:pkg] = File.expand_path(values[:pkg]) if values[:pkg] end # Only set :api_key from SharedValues if :api_key_path isn't set (conflicting options) @@ -117,7 +119,7 @@ def self.authors end def self.is_supported?(platform) - [:ios].include?(platform) + [:ios, :mac, :tvos].include?(platform) end end end diff --git a/fastlane_core/lib/fastlane_core/build_watcher.rb b/fastlane_core/lib/fastlane_core/build_watcher.rb index fc5b65d5984..bec96854dc7 100644 --- a/fastlane_core/lib/fastlane_core/build_watcher.rb +++ b/fastlane_core/lib/fastlane_core/build_watcher.rb @@ -11,7 +11,7 @@ class BuildWatcher class << self # @return The build we waited for. This method will always return a build - def wait_for_build_processing_to_be_complete(app_id: nil, platform: nil, train_version: nil, app_version: nil, build_version: nil, poll_interval: 10, timeout_duration: nil, strict_build_watch: false, return_when_build_appears: false, return_spaceship_testflight_build: true, select_latest: false) + def wait_for_build_processing_to_be_complete(app_id: nil, platform: nil, train_version: nil, app_version: nil, build_version: nil, poll_interval: 10, timeout_duration: nil, strict_build_watch: false, return_when_build_appears: false, return_spaceship_testflight_build: true, select_latest: false, wait_for_build_beta_detail_processing: false) # Warn about train_version being removed in the future if train_version UI.deprecated(":train_version is no longer a used argument on FastlaneCore::BuildWatcher. Please use :app_version instead.") @@ -41,13 +41,13 @@ def wait_for_build_processing_to_be_complete(app_id: nil, platform: nil, train_v showed_info = true end - report_status(build: matched_build) + report_status(build: matched_build, wait_for_build_beta_detail_processing: wait_for_build_beta_detail_processing) # Processing of builds by AppStoreConnect can be a very time consuming task and will # block the worker running this task until it is completed. In some cases, # having a build resource appear in AppStoreConnect (matched_build) may be enough (i.e. setting a changelog) # so here we may choose to skip the full processing of the build if return_when_build_appears is true - if matched_build && (return_when_build_appears || matched_build.processed?) + if matched_build && (return_when_build_appears || processed?(build: matched_build, wait_for_build_beta_detail_processing: wait_for_build_beta_detail_processing)) if !app_version.nil? && app_version != app_version_queried UI.important("App version is #{app_version} but build was found while querying #{app_version_queried}") @@ -145,10 +145,29 @@ def alternate_version(version) return nil end - def report_status(build: nil) - if build && !build.processed? + def processed?(build: nil, wait_for_build_beta_detail_processing: false) + return false unless build + + is_processed = build.processed? + + # App Store Connect API has multiple build processing states + # builds have one processing status + # buildBetaDetails have two processing statues (internal and external testing) + # + # If set, this method will only return true if all three statuses are complete + if wait_for_build_beta_detail_processing + is_processed &&= build.build_beta_detail.processed? + end + + return is_processed + end + + def report_status(build: nil, wait_for_build_beta_detail_processing: false) + is_processed = processed?(build: build, wait_for_build_beta_detail_processing: wait_for_build_beta_detail_processing) + + if build && !is_processed UI.message("Waiting for App Store Connect to finish processing the new build (#{build.app_version} - #{build.version}) for #{build.platform}") - elsif build && build.processed? + elsif build && is_processed UI.success("Successfully finished processing the build #{build.app_version} - #{build.version} for #{build.platform}") else UI.message("Waiting for the build to show up in the build list - this may take a few minutes (check your email for processing issues if this continues)") diff --git a/pilot/lib/pilot/build_manager.rb b/pilot/lib/pilot/build_manager.rb index 7929c155b60..09d4d63b553 100644 --- a/pilot/lib/pilot/build_manager.rb +++ b/pilot/lib/pilot/build_manager.rb @@ -16,7 +16,7 @@ def upload(options) should_login_in_start = options[:apple_id].nil? start(options, should_login: should_login_in_start) - UI.user_error!("No ipa file given") unless config[:ipa] + UI.user_error!("No ipa or pkg file given") if config[:ipa].nil? && config[:pkg].nil? check_for_changelog_or_whats_new!(options) @@ -25,10 +25,17 @@ def upload(options) dir = Dir.mktmpdir platform = fetch_app_platform - package_path = FastlaneCore::IpaUploadPackageBuilder.new.generate(app_id: fetch_app_id, + if options[:ipa] + package_path = FastlaneCore::IpaUploadPackageBuilder.new.generate(app_id: fetch_app_id, ipa_path: options[:ipa], package_path: dir, platform: platform) + else + package_path = FastlaneCore::PkgUploadPackageBuilder.new.generate(app_id: fetch_app_id, + pkg_path: options[:pkg], + package_path: dir, + platform: platform) + end transporter = transporter_for_selected_team(options) result = transporter.upload(package_path: package_path) @@ -94,6 +101,9 @@ def wait_for_build_processing_to_be_complete(return_when_build_appears = false) if config[:ipa] app_version = FastlaneCore::IpaFileAnalyser.fetch_app_version(config[:ipa]) app_build = FastlaneCore::IpaFileAnalyser.fetch_app_build(config[:ipa]) + elsif config[:pkg] + app_version = FastlaneCore::PkgFileAnalyser.fetch_app_version(config[:pkg]) + app_build = FastlaneCore::PkgFileAnalyser.fetch_app_build(config[:pkg]) else app_version = config[:app_version] app_build = config[:build_number] @@ -108,7 +118,8 @@ def wait_for_build_processing_to_be_complete(return_when_build_appears = false) timeout_duration: config[:wait_processing_timeout_duration], return_when_build_appears: return_when_build_appears, return_spaceship_testflight_build: false, - select_latest: config[:distribute_only] + select_latest: config[:distribute_only], + wait_for_build_beta_detail_processing: true ) unless latest_build.app_version == app_version && latest_build.version == app_build diff --git a/pilot/lib/pilot/manager.rb b/pilot/lib/pilot/manager.rb index d4bb43ab749..92c74c65251 100644 --- a/pilot/lib/pilot/manager.rb +++ b/pilot/lib/pilot/manager.rb @@ -73,7 +73,8 @@ def fetch_app_id def fetch_app_identifier result = config[:app_identifier] - result ||= FastlaneCore::IpaFileAnalyser.fetch_app_identifier(config[:ipa]) + result ||= FastlaneCore::IpaFileAnalyser.fetch_app_identifier(config[:ipa]) if config[:ipa] + result ||= FastlaneCore::PkgFileAnalyser.fetch_app_identifier(config[:pkg]) if config[:pkg] result ||= UI.input("Please enter the app's bundle identifier: ") UI.verbose("App identifier (#{result})") return result @@ -82,6 +83,7 @@ def fetch_app_identifier def fetch_app_platform(required: true) result = config[:app_platform] result ||= FastlaneCore::IpaFileAnalyser.fetch_app_platform(config[:ipa]) if config[:ipa] + result ||= FastlaneCore::PkgFileAnalyser.fetch_app_platform(config[:pkg]) if config[:pkg] if required result ||= UI.input("Please enter the app's platform (appletvos, ios, osx): ") UI.user_error!("App Platform must be ios, appletvos, or osx") unless ['ios', 'appletvos', 'osx'].include?(result) diff --git a/pilot/lib/pilot/options.rb b/pilot/lib/pilot/options.rb index bb63cea5938..497def1057a 100644 --- a/pilot/lib/pilot/options.rb +++ b/pilot/lib/pilot/options.rb @@ -48,7 +48,6 @@ def self.available_options env_name: "PILOT_PLATFORM", description: "The platform to use (optional)", optional: true, - default_value: 'ios', verify_block: proc do |value| UI.user_error!("The platform can only be ios, appletvos, or osx") unless ['ios', 'appletvos', 'osx'].include?(value) end), @@ -82,6 +81,26 @@ def self.available_options value = File.expand_path(value) UI.user_error!("Could not find ipa file at path '#{value}'") unless File.exist?(value) UI.user_error!("'#{value}' doesn't seem to be an ipa file") unless value.end_with?(".ipa") + end, + conflicting_options: [:pkg], + conflict_block: proc do |value| + UI.user_error!("You can't use 'ipa' and '#{value.key}' options in one run.") + end), + FastlaneCore::ConfigItem.new(key: :pkg, + short_option: "-P", + optional: true, + env_name: "PILOT_PKG", + description: "Path to your pkg file", + code_gen_sensitive: true, + default_value: Dir["*.pkg"].sort_by { |x| File.mtime(x) }.last, + default_value_dynamic: true, + verify_block: proc do |value| + UI.user_error!("Could not find pkg file at path '#{File.expand_path(value)}'") unless File.exist?(value) + UI.user_error!("'#{value}' doesn't seem to be a pkg file") unless value.end_with?(".pkg") + end, + conflicting_options: [:ipa], + conflict_block: proc do |value| + UI.user_error!("You can't use 'pkg' and '#{value.key}' options in one run.") end), # app review info diff --git a/pilot/spec/manager_spec.rb b/pilot/spec/manager_spec.rb index a0092f16025..6639e640863 100644 --- a/pilot/spec/manager_spec.rb +++ b/pilot/spec/manager_spec.rb @@ -13,6 +13,7 @@ let(:fake_app) { "fake app" } let(:fake_app_identifier) { "fake app_identifier" } let(:fake_ipa) { "fake ipa" } + let(:fake_pkg) { "fake pkg" } describe "what happens on 'start'" do context "when 'config' variable is already set" do @@ -392,6 +393,23 @@ end end + context "when config does not has 'app_identifier' but has 'pkg' path variable" do + before(:each) do + fake_manager.instance_variable_set(:@config, { pkg: fake_pkg }) + + allow(FastlaneCore::PkgFileAnalyser) + .to receive(:fetch_app_identifier) + .with(fake_pkg) + .and_return(fake_app_identifier) + end + + it "uses the FastlaneCore::PkgFileAnalyser with 'pkg' path to find the 'app_identifier' value" do + fetch_app_identifier_result = fake_manager.fetch_app_identifier + + expect(fetch_app_identifier_result).to eq(fake_app_identifier) + end + end + context "when FastlaneCore::IpaFileAnalyser failed to fetch the 'app_identifier' variable" do before(:each) do fake_manager.instance_variable_set(:@config, { ipa: fake_ipa }) @@ -413,76 +431,147 @@ end describe "what happens on fetching the 'app_platform'" do - let(:fake_app_platform) { "ios" } - context "when config has 'app_platform' variable" do - before(:each) do - expect(UI).to receive(:verbose).with("App Platform (#{fake_app_platform})") - fake_manager.instance_variable_set(:@config, { app_platform: fake_app_platform }) + context "ios" do + let(:fake_app_platform) { "ios" } + + before(:each) do + expect(UI).to receive(:verbose).with("App Platform (#{fake_app_platform})") + fake_manager.instance_variable_set(:@config, { app_platform: fake_app_platform }) + end + + it "uses the config 'app_platform' value" do + fetch_app_platform_result = fake_manager.fetch_app_platform + + expect(fetch_app_platform_result).to eq(fake_app_platform) + end end - it "uses the config 'app_platform' value" do - fetch_app_platform_result = fake_manager.fetch_app_platform + context "osx" do + let(:fake_app_platform) { "osx" } + + before(:each) do + expect(UI).to receive(:verbose).with("App Platform (#{fake_app_platform})") + fake_manager.instance_variable_set(:@config, { app_platform: fake_app_platform }) + end + + it "uses the config 'app_platform' value" do + fetch_app_platform_result = fake_manager.fetch_app_platform - expect(fetch_app_platform_result).to eq(fake_app_platform) + expect(fetch_app_platform_result).to eq(fake_app_platform) + end end end - context "when config does not has 'app_platform' but has 'ipa' path variable" do - before(:each) do - expect(UI).to receive(:verbose).with("App Platform (#{fake_app_platform})") - fake_manager.instance_variable_set(:@config, { ipa: fake_ipa }) + context "when config does not have 'app_platform'" do + context "but has 'ipa' path variable" do + let(:fake_app_platform) { "ios" } - allow(FastlaneCore::IpaFileAnalyser) - .to receive(:fetch_app_platform) - .with(fake_ipa) - .and_return(fake_app_platform) + before(:each) do + expect(UI).to receive(:verbose).with("App Platform (#{fake_app_platform})") + fake_manager.instance_variable_set(:@config, { ipa: fake_ipa }) + + allow(FastlaneCore::IpaFileAnalyser) + .to receive(:fetch_app_platform) + .with(fake_ipa) + .and_return(fake_app_platform) + end + + it "uses the FastlaneCore::IpaFileAnalyser with 'ipa' path to find the 'app_platform' value" do + fetch_app_platform_result = fake_manager.fetch_app_platform + + expect(fetch_app_platform_result).to eq(fake_app_platform) + end end - it "uses the FastlaneCore::IpaFileAnalyser with 'ipa' path to find the 'app_platform' value" do - fetch_app_platform_result = fake_manager.fetch_app_platform + context "but has 'pkg' path variable" do + let(:fake_app_platform) { "osx" } + + before(:each) do + expect(UI).to receive(:verbose).with("App Platform (#{fake_app_platform})") + fake_manager.instance_variable_set(:@config, { pkg: fake_pkg }) - expect(fetch_app_platform_result).to eq(fake_app_platform) + allow(FastlaneCore::PkgFileAnalyser) + .to receive(:fetch_app_platform) + .with(fake_pkg) + .and_return(fake_app_platform) + end + + it "uses the FastlaneCore::PkgFileAnalyser with 'pkg' path to find the 'app_platform' value" do + fetch_app_platform_result = fake_manager.fetch_app_platform + + expect(fetch_app_platform_result).to eq(fake_app_platform) + end end end context "when FastlaneCore::IpaFileAnalyser failed to fetch the 'app_platform' variable" do - before(:each) do - expect(UI).to receive(:verbose).with("App Platform (#{fake_app_platform})") - fake_manager.instance_variable_set(:@config, { ipa: fake_ipa }) + context "ios" do + let(:fake_app_platform) { "ios" } - allow(FastlaneCore::IpaFileAnalyser) - .to receive(:fetch_app_platform) - .with(fake_ipa) - .and_return(nil) + before(:each) do + expect(UI).to receive(:verbose).with("App Platform (#{fake_app_platform})") + fake_manager.instance_variable_set(:@config, { ipa: fake_ipa }) + + allow(FastlaneCore::IpaFileAnalyser) + .to receive(:fetch_app_platform) + .with(fake_ipa) + .and_return(nil) + end + + it "asks user to enter the app's platform manually" do + expect(UI).to receive(:input).with("Please enter the app's platform (appletvos, ios, osx): ").and_return(fake_app_platform) + + fetch_app_platform_result = fake_manager.fetch_app_platform + + expect(fetch_app_platform_result).to eq(fake_app_platform) + end end - it "asks user to enter the app's platform manually" do - expect(UI).to receive(:input).with("Please enter the app's platform (appletvos, ios, osx): ").and_return(fake_app_platform) + context "osx" do + let(:fake_app_platform) { "osx" } - fetch_app_platform_result = fake_manager.fetch_app_platform + before(:each) do + expect(UI).to receive(:verbose).with("App Platform (#{fake_app_platform})") + fake_manager.instance_variable_set(:@config, { pkg: fake_pkg }) - expect(fetch_app_platform_result).to eq(fake_app_platform) + allow(FastlaneCore::PkgFileAnalyser) + .to receive(:fetch_app_platform) + .with(fake_pkg) + .and_return(nil) + end + + it "asks user to enter the app's platform manually" do + expect(UI).to receive(:input).with("Please enter the app's platform (appletvos, ios, osx): ").and_return(fake_app_platform) + + fetch_app_platform_result = fake_manager.fetch_app_platform + + expect(fetch_app_platform_result).to eq(fake_app_platform) + end end end context "when FastlaneCore::IpaFileAnalyser failed to fetch the 'app_platform' variable and its not required to enter manually" do - before(:each) do - expect(UI).not_to receive(:verbose).with("App Platform (#{fake_app_platform})") - fake_manager.instance_variable_set(:@config, { ipa: fake_ipa }) + context "ios" do + let(:fake_app_platform) { "ios" } - allow(FastlaneCore::IpaFileAnalyser) - .to receive(:fetch_app_platform) - .with(fake_ipa) - .and_return(nil) - end + before(:each) do + expect(UI).not_to receive(:verbose).with("App Platform (#{fake_app_platform})") + fake_manager.instance_variable_set(:@config, { ipa: fake_ipa }) - it "does not ask user to enter the app's platform manually" do - expect(UI).not_to receive(:input).with("Please enter the app's platform (appletvos, ios, osx): ") + allow(FastlaneCore::IpaFileAnalyser) + .to receive(:fetch_app_platform) + .with(fake_ipa) + .and_return(nil) + end - fetch_app_platform_result = fake_manager.fetch_app_platform(required: false) + it "does not ask user to enter the app's platform manually" do + expect(UI).not_to receive(:input).with("Please enter the app's platform (appletvos, ios, osx): ") - expect(fetch_app_platform_result).to eq(nil) + fetch_app_platform_result = fake_manager.fetch_app_platform(required: false) + + expect(fetch_app_platform_result).to eq(nil) + end end end diff --git a/spaceship/lib/spaceship/connect_api/models/build.rb b/spaceship/lib/spaceship/connect_api/models/build.rb index 279b5ec34ac..0224ac90ce8 100644 --- a/spaceship/lib/spaceship/connect_api/models/build.rb +++ b/spaceship/lib/spaceship/connect_api/models/build.rb @@ -86,6 +86,10 @@ def ready_for_internal_testing? return build_beta_detail.nil? == false && build_beta_detail.ready_for_internal_testing? end + def ready_for_external_testing? + return build_beta_detail.nil? == false && build_beta_detail.ready_for_external_testing? + end + def ready_for_beta_submission? raise "No build_beta_detail included" unless build_beta_detail return build_beta_detail.ready_for_beta_submission? diff --git a/spaceship/lib/spaceship/connect_api/models/build_beta_detail.rb b/spaceship/lib/spaceship/connect_api/models/build_beta_detail.rb index 5e053c0ccb0..ce9e5258ff6 100644 --- a/spaceship/lib/spaceship/connect_api/models/build_beta_detail.rb +++ b/spaceship/lib/spaceship/connect_api/models/build_beta_detail.rb @@ -53,6 +53,10 @@ def ready_for_internal_testing? return internal_build_state == InternalState::READY_FOR_BETA_TESTING end + def processed? + return internal_build_state != InternalState::PROCESSING && external_build_state != ExternalState::PROCESSING + end + def ready_for_beta_submission? return external_build_state == ExternalState::READY_FOR_BETA_SUBMISSION end