pilot/lib/pilot/build_manager.rb in fastlane-2.125.0.beta.20190531200016 vs pilot/lib/pilot/build_manager.rb in fastlane-2.125.0

- old
+ new

@@ -6,41 +6,34 @@ require 'fastlane_core/build_watcher' require 'fastlane_core/ipa_upload_package_builder' require_relative 'manager' module Pilot + # rubocop:disable Metrics/ClassLength class BuildManager < Manager def upload(options) # Only need to login before upload if no apple_id was given # 'login' will be deferred until before waiting for build processing should_login_in_start = options[:apple_id].nil? start(options, should_login: should_login_in_start) - options[:changelog] = self.class.sanitize_changelog(options[:changelog]) if options[:changelog] - UI.user_error!("No ipa file given") unless config[:ipa] - if options[:changelog].nil? && options[:distribute_external] == true - if UI.interactive? - options[:changelog] = UI.input("No changelog provided for new build. You can provide a changelog using the `changelog` option. For now, please provide a changelog here:") - else - UI.user_error!("No changelog provided for new build. Please either disable `distribute_external` or provide a changelog using the `changelog` option") - end - end + check_for_changelog_or_whats_new!(options) - UI.success("Ready to upload new build to TestFlight (App: #{fetch_apple_id})...") + UI.success("Ready to upload new build to TestFlight (App: #{fetch_app_id})...") dir = Dir.mktmpdir platform = fetch_app_platform - package_path = FastlaneCore::IpaUploadPackageBuilder.new.generate(app_id: fetch_apple_id, + package_path = FastlaneCore::IpaUploadPackageBuilder.new.generate(app_id: fetch_app_id, ipa_path: options[:ipa], package_path: dir, platform: platform) transporter = transporter_for_selected_team(options) - result = transporter.upload(fetch_apple_id, package_path) + result = transporter.upload(fetch_app_id, package_path) unless result UI.user_error!("Error uploading ipa file, for more information see above") end @@ -58,18 +51,45 @@ UI.message("If you want to skip waiting for the processing to be finished, use the `skip_waiting_for_build_processing` option") latest_build = wait_for_build_processing_to_be_complete distribute(options, build: latest_build) end + def has_changelog_or_whats_new?(options) + # Look for legacy :changelog option + has_changelog = !options[:changelog].nil? + + # Look for :whats_new in :localized_build_info + unless has_changelog + infos_by_lang = options[:localized_build_info] || [] + infos_by_lang.each do |k, v| + next if has_changelog + v ||= {} + has_changelog = v.key?(:whats_new) || v.key?('whats_new') + end + end + + return has_changelog + end + + def check_for_changelog_or_whats_new!(options) + if !has_changelog_or_whats_new?(options) && options[:distribute_external] == true + if UI.interactive? + options[:changelog] = UI.input("No changelog provided for new build. You can provide a changelog using the `changelog` option. For now, please provide a changelog here:") + else + UI.user_error!("No changelog provided for new build. Please either disable `distribute_external` or provide a changelog using the `changelog` option") + end + end + end + def wait_for_build_processing_to_be_complete platform = fetch_app_platform app_version = FastlaneCore::IpaFileAnalyser.fetch_app_version(config[:ipa]) app_build = FastlaneCore::IpaFileAnalyser.fetch_app_build(config[:ipa]) - latest_build = FastlaneCore::BuildWatcher.wait_for_build_processing_to_be_complete(app_id: app.apple_id, platform: platform, train_version: app_version, build_version: app_build, poll_interval: config[:wait_processing_interval], strict_build_watch: config[:wait_for_uploaded_build]) + latest_build = FastlaneCore::BuildWatcher.wait_for_build_processing_to_be_complete(app_id: app.id, platform: platform, train_version: app_version, build_version: app_build, poll_interval: config[:wait_processing_interval], return_spaceship_testflight_build: false) - unless latest_build.train_version == app_version && latest_build.build_version == app_build - UI.important("Uploaded app #{app_version} - #{app_build}, but received build #{latest_build.train_version} - #{latest_build.build_version}.") + unless latest_build.app_version == app_version && latest_build.version == app_build + UI.important("Uploaded app #{app_version} - #{app_build}, but received build #{latest_build.app_version} - #{latest_build.version}.") end return latest_build end @@ -77,11 +97,22 @@ start(options) if config[:apple_id].to_s.length == 0 && config[:app_identifier].to_s.length == 0 config[:app_identifier] = UI.input("App Identifier: ") end - build ||= Spaceship::TestFlight::Build.latest(app_id: app.apple_id, platform: fetch_app_platform) + # Get latest uploaded build if no build specified + build ||= Spaceship::ConnectAPI::Build.all(app_id: app.id, sort: "-uploadedDate", limit: 1).first + + # Verify the build has all the includes that we need + # and fetch a new build if not + if build && (!build.app || !build.build_beta_detail || !build.pre_release_version) + UI.important("Build did include information for app, build beta detail and pre release version") + UI.important("Fetching a new build with all the information needed") + build = Spaceship::ConnectAPI::Build.get(build_id: build.id) + end + + # Error out if no build if build.nil? UI.user_error!("No build to distribute!") end # Update beta app meta info @@ -92,16 +123,16 @@ # 5. Auto notify enabled with config[:notify_external_testers] update_beta_app_meta(options, build) return if config[:skip_submission] if options[:reject_build_waiting_for_review] - waiting_for_review_build = Spaceship::TestFlight::Build.all_waiting_for_review(app_id: build.app_id, platform: fetch_app_platform).first + waiting_for_review_build = build.app.get_builds(filter: { "betaAppReviewSubmission.betaReviewState" => "WAITING_FOR_REVIEW" }, includes: "betaAppReviewSubmission,preReleaseVersion").first unless waiting_for_review_build.nil? - UI.important("Another build is already in review. Going to expire that build and submit the new one.") - UI.important("Expiring build: #{waiting_for_review_build.train_version} - #{waiting_for_review_build.build_version}") - waiting_for_review_build.expire! - UI.success("Expired previous build: #{waiting_for_review_build.train_version} - #{waiting_for_review_build.build_version}") + UI.important("Another build is already in review. Going to remove that build and submit the new one.") + UI.important("Deleting beta app review submission for build: #{waiting_for_review_build.app_version} - #{waiting_for_review_build.version}") + waiting_for_review_build.beta_app_review_submission.delete! + UI.success("Deleted beta app review submission for previous build: #{waiting_for_review_build.app_version} - #{waiting_for_review_build.version}") end end distribute_build(build, options) type = options[:distribute_external] ? 'External' : 'Internal' UI.success("Successfully distributed build to #{type} testers 🚀") @@ -111,60 +142,77 @@ start(options) if config[:apple_id].to_s.length == 0 && config[:app_identifier].to_s.length == 0 config[:app_identifier] = UI.input("App Identifier: ") end - platform = fetch_app_platform(required: false) - builds = app.all_processing_builds(platform: platform) + app.builds(platform: platform) - # sort by upload_date - builds.sort! { |a, b| a.upload_date <=> b.upload_date } - rows = builds.collect { |build| describe_build(build) } + # Get processing builds + build_deliveries = app.get_build_deliveries.map do |build_delivery| + [ + build_delivery.cf_build_short_version_string, + build_delivery.cf_build_version + ] + end + # Get processed builds + builds = app.get_builds(includes: "betaBuildMetrics,preReleaseVersion", sort: "-uploadedDate").map do |build| + [ + build.app_version, + build.version, + build.beta_build_metrics.map(&:install_count).reduce(:+) + ] + end + + # Only show table if there are any build deliveries + unless build_deliveries.empty? + puts(Terminal::Table.new( + title: "#{app.name} Processing Builds".green, + headings: ["Version #", "Build #"], + rows: FastlaneCore::PrintTable.transform_output(build_deliveries) + )) + end + puts(Terminal::Table.new( title: "#{app.name} Builds".green, headings: ["Version #", "Build #", "Installs"], - rows: FastlaneCore::PrintTable.transform_output(rows) + rows: FastlaneCore::PrintTable.transform_output(builds) )) end def update_beta_app_meta(options, build) - # App Store Connect API build id - build_id = build.find_app_store_connect_build["id"] - # Setting account required wth AppStore Connect API - update_review_detail(build.app_id, { demo_account_required: options[:demo_account_required] }) + update_review_detail(build, { demo_account_required: options[:demo_account_required] }) if should_update_beta_app_review_info(options) - update_review_detail(build.app_id, options[:beta_app_review_info]) + update_review_detail(build, options[:beta_app_review_info]) end if should_update_localized_app_information?(options) - update_localized_app_review(build.app_id, options[:localized_app_info]) + update_localized_app_review(build, options[:localized_app_info]) elsif should_update_app_test_information?(options) default_info = {} default_info[:feedback_email] = options[:beta_app_feedback_email] if options[:beta_app_feedback_email] default_info[:description] = options[:beta_app_description] if options[:beta_app_description] begin - update_localized_app_review(build.app_id, {}, default_info: default_info) + update_localized_app_review(build, {}, default_info: default_info) UI.success("Successfully set the beta_app_feedback_email and/or beta_app_description") rescue => ex UI.user_error!("Could not set beta_app_feedback_email and/or beta_app_description: #{ex}") end end if should_update_localized_build_information?(options) - update_localized_build_review(build_id, options[:localized_build_info]) + update_localized_build_review(build, options[:localized_build_info]) elsif should_update_build_information?(options) begin - update_localized_build_review(build_id, {}, default_info: { whats_new: options[:changelog] }) + update_localized_build_review(build, {}, default_info: { whats_new: options[:changelog] }) UI.success("Successfully set the changelog for build") rescue => ex UI.user_error!("Could not set changelog: #{ex}") end end - update_build_beta_details(build_id, { + update_build_beta_details(build, { auto_notify_enabled: options[:notify_external_testers] }) end def self.truncate_changelog(changelog) @@ -240,41 +288,31 @@ return generic_transporter end end def distribute_build(uploaded_build, options) - UI.message("Distributing new build to testers: #{uploaded_build.train_version} - #{uploaded_build.build_version}") + UI.message("Distributing new build to testers: #{uploaded_build.app_version} - #{uploaded_build.version}") # This is where we could add a check to see if encryption is required and has been updated set_export_compliance_if_needed(uploaded_build, options) if options[:groups] || options[:distribute_external] - begin - uploaded_build.submit_for_testflight_review! - rescue => ex - # App Store Connect currently may 504 on this request even though it manages to get the build in - # the approved state, this is a temporary workaround. - raise ex unless ex.to_s.include?("504") - UI.message("Submitting the build for review timed out, trying to recover.") - updated_build = Spaceship::TestFlight::Build.find(app_id: uploaded_build.app_id, build_id: uploaded_build.id) - raise ex unless updated_build.approved? + if uploaded_build.ready_for_beta_submission? + uploaded_build.post_beta_app_review_submission + else + UI.message("Build #{uploaded_build.app_version} - #{uploaded_build.version} already submitted for review") end end if options[:groups] - client = Spaceship::ConnectAPI::Base.client - beta_group_ids = client.get_beta_groups(filter: { app: uploaded_build.app_id }).select do |group| - options[:groups].include?(group["attributes"]["name"]) - end.map do |group| - group["id"] + app = uploaded_build.app + beta_groups = app.get_beta_groups.select do |group| + options[:groups].include?(group.name) end - unless beta_group_ids.empty? - build = uploaded_build.find_app_store_connect_build - build_id = build["id"] - - client.add_beta_groups_to_build(build_id: build_id, beta_group_ids: beta_group_ids) + unless beta_groups.empty? + uploaded_build.add_beta_groups(beta_groups: beta_groups) end end if options[:distribute_external] && options[:groups].nil? # Legacy Spaceship::TestFlight API used to have a `default_external_group` that would automatically @@ -284,26 +322,24 @@ true end def set_export_compliance_if_needed(uploaded_build, options) - build = uploaded_build.find_app_store_connect_build - build_attributes = build["attributes"] || {} - if build_attributes["usesNonExemptEncryption"].nil? + if uploaded_build.uses_non_exempt_encryption.nil? uses_non_exempt_encryption = options[:uses_non_exempt_encryption] attributes = { usesNonExemptEncryption: uses_non_exempt_encryption } client = Spaceship::ConnectAPI::Base.client - client.patch_builds(build_id: build["id"], attributes: attributes) + client.patch_builds(build_id: uploaded_build.id, attributes: attributes) UI.important("Export compliance has been set to '#{uses_non_exempt_encryption}'. Need to wait for build to finishing processing again...") UI.important("Set 'ITSAppUsesNonExemptEncryption' in the 'Info.plist' to skip this step and speed up the submission") wait_for_build_processing_to_be_complete end end - def update_review_detail(app_id, info) + def update_review_detail(build, info) info = info.collect { |k, v| [k.to_sym, v] }.to_h attributes = {} attributes[:contactEmail] = info[:contact_email] if info.key?(:contact_email) attributes[:contactFirstName] = info[:contact_first_name] if info.key?(:contact_first_name) @@ -313,114 +349,110 @@ attributes[:demoAccountPassword] = info[:demo_account_password] if info.key?(:demo_account_password) attributes[:demoAccountRequired] = info[:demo_account_required] if info.key?(:demo_account_required) attributes[:notes] = info[:notes] if info.key?(:notes) client = Spaceship::ConnectAPI::Base.client - client.patch_beta_app_review_detail(app_id: app_id, attributes: attributes) + client.patch_beta_app_review_detail(app_id: build.app.id, attributes: attributes) end - def update_localized_app_review(app_id, info_by_lang, default_info: nil) + def update_localized_app_review(build, info_by_lang, default_info: nil) info_by_lang = info_by_lang.collect { |k, v| [k.to_sym, v] }.to_h if default_info info_by_lang.delete(:default) else default_info = info_by_lang.delete(:default) end - # Initialize hash of lang codes - langs_localization_ids = {} + # Initialize hash of lang codes with info_by_lang keys + localizations_by_lang = {} + info_by_lang.each_key do |key| + localizations_by_lang[key] = nil + end # Validate locales exist - client = Spaceship::ConnectAPI::Base.client - localizations = client.get_beta_app_localizations(filter: { app: app_id }) + localizations = app.get_beta_app_localizations localizations.each do |localization| - localization_id = localization["id"] - attributes = localization["attributes"] - locale = attributes["locale"] - - langs_localization_ids[locale.to_sym] = localization_id + localizations_by_lang[localization.locale.to_sym] = localization end # Create or update localized app review info - langs_localization_ids.each do |lang_code, localization_id| + localizations_by_lang.each do |lang_code, localization| info = info_by_lang[lang_code] info = default_info unless info - update_localized_app_review_for_lang(app_id, localization_id, lang_code, info) if info + update_localized_app_review_for_lang(app, localization, lang_code, info) if info end end - def update_localized_app_review_for_lang(app_id, localization_id, locale, info) + def update_localized_app_review_for_lang(app, localization, locale, info) attributes = {} attributes[:feedbackEmail] = info[:feedback_email] if info.key?(:feedback_email) attributes[:marketingUrl] = info[:marketing_url] if info.key?(:marketing_url) attributes[:privacyPolicyUrl] = info[:privacy_policy_url] if info.key?(:privacy_policy_url) attributes[:tvOsPrivacyPolicy] = info[:tv_os_privacy_policy_url] if info.key?(:tv_os_privacy_policy_url) attributes[:description] = info[:description] if info.key?(:description) client = Spaceship::ConnectAPI::Base.client - if localization_id - client.patch_beta_app_localizations(localization_id: localization_id, attributes: attributes) + if localization + client.patch_beta_app_localizations(localization_id: localization.id, attributes: attributes) else attributes[:locale] = locale if locale - client.post_beta_app_localizations(app_id: app_id, attributes: attributes) + client.post_beta_app_localizations(app_id: app.id, attributes: attributes) end end - def update_localized_build_review(build_id, info_by_lang, default_info: nil) + def update_localized_build_review(build, info_by_lang, default_info: nil) info_by_lang = info_by_lang.collect { |k, v| [k.to_sym, v] }.to_h if default_info info_by_lang.delete(:default) else default_info = info_by_lang.delete(:default) end - # Initialize hash of lang codes - langs_localization_ids = {} + # Initialize hash of lang codes with info_by_lang keys + localizations_by_lang = {} + info_by_lang.each_key do |key| + localizations_by_lang[key] = nil + end # Validate locales exist - client = Spaceship::ConnectAPI::Base.client - localizations = client.get_beta_build_localizations(filter: { build: build_id }) + localizations = build.get_beta_build_localizations localizations.each do |localization| - localization_id = localization["id"] - attributes = localization["attributes"] - locale = attributes["locale"] - - langs_localization_ids[locale.to_sym] = localization_id + localizations_by_lang[localization.locale.to_sym] = localization end # Create or update localized app review info - langs_localization_ids.each do |lang_code, localization_id| + localizations_by_lang.each do |lang_code, localization| info = info_by_lang[lang_code] info = default_info unless info - update_localized_build_review_for_lang(build_id, localization_id, lang_code, info) if info + update_localized_build_review_for_lang(build, localization, lang_code, info) if info end end - def update_localized_build_review_for_lang(build_id, localization_id, locale, info) + def update_localized_build_review_for_lang(build, localization, locale, info) attributes = {} - attributes[:whatsNew] = info[:whats_new] if info.key?(:whats_new) + attributes[:whatsNew] = self.class.sanitize_changelog(info[:whats_new]) if info.key?(:whats_new) client = Spaceship::ConnectAPI::Base.client - if localization_id - client.patch_beta_build_localizations(localization_id: localization_id, attributes: attributes) + if localization + client.patch_beta_build_localizations(localization_id: localization.id, attributes: attributes) else attributes[:locale] = locale if locale - client.post_beta_build_localizations(build_id: build_id, attributes: attributes) + client.post_beta_build_localizations(build_id: build.id, attributes: attributes) end end - def update_build_beta_details(build_id, info) - client = Spaceship::ConnectAPI::Base.client - resp = client.get_build_beta_details(filter: { build: build_id }) - build_beta_details_id = resp.first["id"] + def update_build_beta_details(build, info) + build_beta_detail = build.build_beta_detail attributes = {} attributes[:autoNotifyEnabled] = info[:auto_notify_enabled] if info.key?(:auto_notify_enabled) - client.patch_build_beta_details(build_beta_details_id: build_beta_details_id, attributes: attributes) + client = Spaceship::ConnectAPI::Base.client + client.patch_build_beta_details(build_beta_details_id: build_beta_detail.id, attributes: attributes) end end + # rubocop:enable Metrics/ClassLength end