diff --git a/match/lib/match.rb b/match/lib/match.rb index 0a279a0275c..e0624e4c92f 100644 --- a/match/lib/match.rb +++ b/match/lib/match.rb @@ -12,3 +12,6 @@ require_relative 'match/storage' require_relative 'match/encryption' require_relative 'match/module' +require_relative 'match/portal_cache' +require_relative 'match/portal_fetcher' +require_relative 'match/profile_includes' diff --git a/match/lib/match/generator.rb b/match/lib/match/generator.rb index 631b3574de3..5fb7c2a0742 100644 --- a/match/lib/match/generator.rb +++ b/match/lib/match/generator.rb @@ -1,4 +1,5 @@ require_relative 'module' +require_relative 'profile_includes' module Match # Generate missing resources @@ -57,7 +58,7 @@ def self.generate_certificate(params, cert_type, working_directory, specific_cer end # @return (String) The UUID of the newly generated profile - def self.generate_provisioning_profile(params: nil, prov_type: nil, certificate_id: nil, app_identifier: nil, force: true, working_directory: nil) + def self.generate_provisioning_profile(params: nil, prov_type: nil, certificate_id: nil, app_identifier: nil, force: true, cache: nil, working_directory: nil) require 'sigh/manager' require 'sigh/options' @@ -104,6 +105,13 @@ def self.generate_provisioning_profile(params: nil, prov_type: nil, certificate_ values[:development] = true end + if cache + values[:cached_certificates] = cache.certificates + values[:cached_devices] = cache.devices + values[:cached_bundle_ids] = cache.bundle_ids + values[:cached_profiles] = cache.profiles + end + arguments = FastlaneCore::Configuration.create(Sigh::Options.available_options, values) Sigh.config = arguments diff --git a/match/lib/match/module.rb b/match/lib/match/module.rb index 4aa2a8db2a5..294ca5cea2b 100644 --- a/match/lib/match/module.rb +++ b/match/lib/match/module.rb @@ -23,7 +23,8 @@ def self.profile_type_sym(type) end def self.cert_type_sym(type) - type = type.to_s + # To determine certificate types to fetch from the portal, we use `Sigh.certificate_types_for_profile_and_platform`, and it returns typed `Spaceship::ConnectAPI::Certificate::CertificateType` with the same values but uppercased, so we downcase them here + type = type.to_s.downcase return :mac_installer_distribution if type == "mac_installer_distribution" return :developer_id_installer if type == "developer_id_installer" return :developer_id_application if type == "developer_id" diff --git a/match/lib/match/portal_cache.rb b/match/lib/match/portal_cache.rb new file mode 100644 index 00000000000..dc90e8e2652 --- /dev/null +++ b/match/lib/match/portal_cache.rb @@ -0,0 +1,106 @@ +require 'fastlane_core/provisioning_profile' +require 'spaceship/client' +require_relative 'portal_fetcher' +module Match + class Portal + class Cache + def self.build(params:, bundle_id_identifiers:) + require_relative 'profile_includes' + require 'sigh' + + profile_type = Sigh.profile_type_for_distribution_type( + platform: params[:platform], + distribution_type: params[:type] + ) + + cache = Portal::Cache.new( + platform: params[:platform], + profile_type: profile_type, + additional_cert_types: params[:additional_cert_types], + bundle_id_identifiers: bundle_id_identifiers, + needs_profiles_devices: ProfileIncludes.can_force_include?(params: params, notify: true) && !params[:force] && !params[:readonly], + needs_profiles_certificate_content: !ProfileIncludes.can_force_include_all_certificates?(params: params), + include_mac_in_profiles: params[:include_mac_in_profiles] + ) + + return cache + end + + attr_reader :platform, :profile_type, :bundle_id_identifiers, :additional_cert_types, :needs_profiles_devices, :needs_profiles_certificate_content, :include_mac_in_profiles + + def initialize(platform:, profile_type:, additional_cert_types:, bundle_id_identifiers:, needs_profiles_devices:, needs_profiles_certificate_content:, include_mac_in_profiles:) + @platform = platform + @profile_type = profile_type + + # Bundle Ids + @bundle_id_identifiers = bundle_id_identifiers + + # Certs + @additional_cert_types = additional_cert_types + + # Profiles + @needs_profiles_devices = needs_profiles_devices + @needs_profiles_certificate_content = needs_profiles_certificate_content + + # Devices + @include_mac_in_profiles = include_mac_in_profiles + end + + def portal_profile(stored_profile_path:, keychain_path:) + parsed = FastlaneCore::ProvisioningProfile.parse(stored_profile_path, keychain_path) + uuid = parsed["UUID"] + + portal_profile = self.profiles.detect { |i| i.uuid == uuid } + + portal_profile + end + + def reset_certificates + @certificates = nil + end + + def forget_portal_profile(portal_profile) + return unless @profiles && portal_profile + + @profiles -= [portal_profile] + end + + def bundle_ids + @bundle_ids ||= Match::Portal::Fetcher.bundle_ids( + bundle_id_identifiers: @bundle_id_identifiers + ) + + return @bundle_ids.dup + end + + def certificates + @certificates ||= Match::Portal::Fetcher.certificates( + platform: @platform, + profile_type: @profile_type, + additional_cert_types: @additional_cert_types + ) + + return @certificates.dup + end + + def profiles + @profiles ||= Match::Portal::Fetcher.profiles( + profile_type: @profile_type, + needs_profiles_devices: @needs_profiles_devices, + needs_profiles_certificate_content: @needs_profiles_certificate_content + ) + + return @profiles.dup + end + + def devices + @devices ||= Match::Portal::Fetcher.devices( + platform: @platform, + include_mac_in_profiles: @include_mac_in_profiles + ) + + return @devices.dup + end + end + end +end diff --git a/match/lib/match/portal_fetcher.rb b/match/lib/match/portal_fetcher.rb new file mode 100644 index 00000000000..74dde64c5f7 --- /dev/null +++ b/match/lib/match/portal_fetcher.rb @@ -0,0 +1,72 @@ +require 'fastlane_core/provisioning_profile' +require 'spaceship/client' +require 'spaceship/connect_api/models/profile' + +module Match + class Portal + module Fetcher + def self.profiles(profile_type:, needs_profiles_devices: false, needs_profiles_certificate_content: false, name: nil) + includes = ['bundleId'] + + if needs_profiles_devices + includes += ['devices', 'certificates'] + end + + if needs_profiles_certificate_content + includes += ['certificates'] + end + + profiles = Spaceship::ConnectAPI::Profile.all( + filter: { profileType: profile_type, name: name }.compact, + includes: includes.uniq.join(',') + ) + + profiles + end + + def self.certificates(platform:, profile_type:, additional_cert_types:) + require 'sigh' + certificate_types = Sigh.certificate_types_for_profile_and_platform(platform: platform, profile_type: profile_type) + + additional_cert_types ||= [] + additional_cert_types.map! do |cert_type| + case Match.cert_type_sym(cert_type) + when :mac_installer_distribution + Spaceship::ConnectAPI::Certificate::CertificateType::MAC_INSTALLER_DISTRIBUTION + when :developer_id_installer + Spaceship::ConnectAPI::Certificate::CertificateType::DEVELOPER_ID_INSTALLER + end + end + + certificate_types += additional_cert_types + + filter = { certificateType: certificate_types.uniq.sort.join(',') } unless certificate_types.empty? + + certificates = Spaceship::ConnectAPI::Certificate.all( + filter: filter + ).select(&:valid?) + + certificates + end + + def self.devices(platform: nil, include_mac_in_profiles: false) + devices = Spaceship::ConnectAPI::Device.devices_for_platform( + platform: platform, + include_mac_in_profiles: include_mac_in_profiles + ) + + devices + end + + def self.bundle_ids(bundle_id_identifiers: nil) + filter = { identifier: bundle_id_identifiers.join(',') } if bundle_id_identifiers + + bundle_ids = Spaceship::ConnectAPI::BundleId.all( + filter: filter + ) + + bundle_ids + end + end + end +end diff --git a/match/lib/match/profile_includes.rb b/match/lib/match/profile_includes.rb new file mode 100644 index 00000000000..38cce80bc7c --- /dev/null +++ b/match/lib/match/profile_includes.rb @@ -0,0 +1,120 @@ +require_relative 'portal_fetcher' +require_relative 'module' + +module Match + class ProfileIncludes + PROV_TYPES_WITH_DEVICES = [:adhoc, :development] + PROV_TYPES_WITH_MULTIPLE_CERTIFICATES = [:development] + + def self.can_force_include?(params:, notify:) + self.can_force_include_all_devices?(params: params, notify: notify) && + self.can_force_include_all_certificates?(params: params, notify: notify) + end + + ############### + # + # DEVICES + # + ############### + + def self.should_force_include_all_devices?(params:, portal_profile:, cached_devices:) + return false unless self.can_force_include_all_devices?(params: params) + + force = devices_differ?(portal_profile: portal_profile, platform: params[:platform], include_mac_in_profiles: params[:include_mac_in_profiles], cached_devices: cached_devices) + + return force + end + + def self.can_force_include_all_devices?(params:, notify: false) + return false if params[:readonly] || params[:force] + return false unless params[:force_for_new_devices] + + provisioning_type = params[:type].to_sym + + can_force = PROV_TYPES_WITH_DEVICES.include?(provisioning_type) + + if !can_force && notify + # App Store provisioning profiles don't contain device identifiers and + # thus shouldn't be renewed if the device count has changed. + UI.important("Warning: `force_for_new_devices` is set but is ignored for #{provisioning_type}.") + UI.important("You can safely stop specifying `force_for_new_devices` when running Match for type '#{provisioning_type}'.") + end + + can_force + end + + def self.devices_differ?(portal_profile:, platform:, include_mac_in_profiles:, cached_devices:) + return false unless portal_profile + + profile_devices = portal_profile.devices + + portal_devices = cached_devices + portal_devices ||= Match::Portal::Fetcher.devices(platform: platform, include_mac_in_profiles: include_mac_in_profiles) + + profile_device_ids = profile_devices.map(&:id).sort + portal_devices_ids = portal_devices.map(&:id).sort + + devices_differs = profile_device_ids != portal_devices_ids + + UI.important("Devices in the profile and available on the portal differ. Recreating a profile") if devices_differs + + return devices_differs + end + + ############### + # + # CERTIFICATES + # + ############### + + def self.should_force_include_all_certificates?(params:, portal_profile:, cached_certificates:) + return false unless self.can_force_include_all_certificates?(params: params) + + force = certificates_differ?(portal_profile: portal_profile, platform: params[:platform], cached_certificates: cached_certificates) + + return force + end + + def self.can_force_include_all_certificates?(params:, notify: false) + return false if params[:readonly] || params[:force] + return false unless params[:force_for_new_certificates] + + unless params[:include_all_certificates] + UI.important("You specified 'force_for_new_certificates: true', but new certificates will not be added, cause 'include_all_certificates' is 'false'") if notify + return false + end + + provisioning_type = params[:type].to_sym + + can_force = PROV_TYPES_WITH_MULTIPLE_CERTIFICATES.include?(provisioning_type) + + if !can_force && notify + # All other (not development) provisioning profiles don't contain + # multiple certificates, thus shouldn't be renewed + # if the certificates count has changed. + UI.important("Warning: `force_for_new_certificates` is set but is ignored for non-'development' provisioning profiles.") + UI.important("You can safely stop specifying `force_for_new_certificates` when running Match for '#{provisioning_type}' provisioning profiles.") + end + + can_force + end + + def self.certificates_differ?(portal_profile:, platform:, cached_certificates:) + return false unless portal_profile + + profile_certs = portal_profile.certificates + + portal_certs = cached_certificates + portal_certs ||= Match::Portal::Fetcher.certificates(platform: platform, profile_type: portal_profile.profile_type) + + profile_certs_ids = profile_certs.map(&:id).sort + portal_certs_ids = portal_certs.map(&:id).sort + + certificates_differ = profile_certs_ids != portal_certs_ids + + UI.important("Certificates in the profile and available on the portal differ. Recreating a profile") if certificates_differ + + return certificates_differ + end + end +end diff --git a/match/lib/match/runner.rb b/match/lib/match/runner.rb index 9e5fec54a8c..7f5fd446d4b 100644 --- a/match/lib/match/runner.rb +++ b/match/lib/match/runner.rb @@ -2,6 +2,8 @@ require 'fastlane_core/provisioning_profile' require 'fastlane_core/print_table' require 'spaceship/client' +require 'sigh/module' + require_relative 'generator' require_relative 'module' require_relative 'table_printer' @@ -10,6 +12,8 @@ require_relative 'storage' require_relative 'encryption' +require_relative 'profile_includes' +require_relative 'portal_cache' module Match # rubocop:disable Metrics/ClassLength @@ -19,6 +23,8 @@ class Runner attr_accessor :storage + attr_accessor :cache + # rubocop:disable Metrics/PerceivedComplexity def run(params) self.files_to_commit = [] @@ -60,12 +66,18 @@ def run(params) # sometimes we get an array with arrays, this is a bug. To unblock people using match, I suggest we flatten # then in the future address the root cause of https://github.com/fastlane/fastlane/issues/11324 - app_identifiers = app_identifiers.flatten + app_identifiers = app_identifiers.flatten.uniq + + # Cache bundle ids, certificates, profiles, and devices. + self.cache = Portal::Cache.build( + params: params, + bundle_id_identifiers: app_identifiers + ) # Verify the App ID (as we don't want 'match' to fail at a later point) if spaceship app_identifiers.each do |app_identifier| - spaceship.bundle_identifier_exists(username: params[:username], app_identifier: app_identifier, platform: params[:platform]) + spaceship.bundle_identifier_exists(username: params[:username], app_identifier: app_identifier, cached_bundle_ids: self.cache.bundle_ids) end end @@ -78,17 +90,24 @@ def run(params) fetch_certificate(params: params, working_directory: storage.working_directory, specific_cert_type: additional_cert_type) end + profile_type = Sigh.profile_type_for_distribution_type( + platform: params[:platform], + distribution_type: params[:type] + ) + cert_ids << cert_id - spaceship.certificates_exists(username: params[:username], certificate_ids: cert_ids) if spaceship + spaceship.certificates_exists(username: params[:username], certificate_ids: cert_ids, platform: params[:platform], profile_type: profile_type, cached_certificates: self.cache.certificates) if spaceship # Provisioning Profiles + unless params[:skip_provisioning_profiles] app_identifiers.each do |app_identifier| loop do break if fetch_provisioning_profile(params: params, + profile_type: profile_type, certificate_id: cert_id, app_identifier: app_identifier, - working_directory: storage.working_directory) + working_directory: storage.working_directory) end end end @@ -147,6 +166,9 @@ def fetch_certificate(params: nil, working_directory: nil, specific_cert_type: n self.files_to_commit << cert_path self.files_to_commit << private_key_path + + # Reset certificates cache since we have a new cert. + self.cache.reset_certificates else cert_path = select_cert_or_key(paths: certs) @@ -199,7 +221,7 @@ def select_cert_or_key(paths:) # rubocop:disable Metrics/PerceivedComplexity # @return [String] The UUID of the provisioning profile so we can verify it with the Apple Developer Portal - def fetch_provisioning_profile(params: nil, certificate_id: nil, app_identifier: nil, working_directory: nil) + def fetch_provisioning_profile(params: nil, profile_type:, certificate_id: nil, app_identifier: nil, working_directory: nil) prov_type = Match.profile_type_sym(params[:type]) names = [Match::Generator.profile_type_name(prov_type), app_identifier] @@ -222,20 +244,23 @@ def fetch_provisioning_profile(params: nil, certificate_id: nil, app_identifier: end # Install the provisioning profiles - profile = profiles.last + stored_profile_path = profiles.last force = params[:force] + portal_profile = self.cache.portal_profile(stored_profile_path: stored_profile_path, keychain_path: keychain_path) if stored_profile_path + if params[:force_for_new_devices] - force = should_force_include_all_devices(params: params, prov_type: prov_type, profile: profile, keychain_path: keychain_path) unless force + force ||= ProfileIncludes.should_force_include_all_devices?(params: params, portal_profile: portal_profile, cached_devices: self.cache.devices) end if params[:include_all_certificates] # Clearing specified certificate id which will prevent a profile being created with only one certificate certificate_id = nil - force = should_force_include_all_certificates(params: params, prov_type: prov_type, profile: profile, keychain_path: keychain_path) unless force + force ||= ProfileIncludes.should_force_include_all_certificates?(params: params, portal_profile: portal_profile, cached_certificates: self.cache.certificates) end - if profile.nil? || force + is_new_profile_created = false + if stored_profile_path.nil? || force if params[:readonly] UI.error("No matching provisioning profiles found for '#{profile_file}'") UI.error("A new one cannot be created because you enabled `readonly`") @@ -248,35 +273,50 @@ def fetch_provisioning_profile(params: nil, certificate_id: nil, app_identifier: UI.user_error!("No matching provisioning profiles found and can not create a new one because you enabled `readonly`. Check the output above for more information.") end - profile = Generator.generate_provisioning_profile(params: params, - prov_type: prov_type, - certificate_id: certificate_id, - app_identifier: app_identifier, - force: force, - working_directory: prefixed_working_directory) - self.files_to_commit << profile - end + stored_profile_path = Generator.generate_provisioning_profile( + params: params, + prov_type: prov_type, + certificate_id: certificate_id, + app_identifier: app_identifier, + force: force, + cache: self.cache, + working_directory: prefixed_working_directory + ) - if Helper.mac? - installed_profile = FastlaneCore::ProvisioningProfile.install(profile, keychain_path) + # Recreation of the profile means old profile is invalid. + # Removing it from cache. We don't need a new profile in cache. + self.cache.forget_portal_profile(portal_profile) if portal_profile + + self.files_to_commit << stored_profile_path + + is_new_profile_created = true end - parsed = FastlaneCore::ProvisioningProfile.parse(profile, keychain_path) + + parsed = FastlaneCore::ProvisioningProfile.parse(stored_profile_path, keychain_path) uuid = parsed["UUID"] + name = parsed["Name"] - if params[:output_path] - FileUtils.cp(profile, params[:output_path]) - end + check_profile_existance = !is_new_profile_created && spaceship + if check_profile_existance && !spaceship.profile_exists(profile_type: profile_type, + name: name, + username: params[:username], + uuid: uuid, + cached_profiles: self.cache.profiles) - if spaceship && !spaceship.profile_exists(type: prov_type, - username: params[:username], - uuid: uuid, - platform: params[:platform]) # This profile is invalid, let's remove the local file and generate a new one - File.delete(profile) + File.delete(stored_profile_path) # This method will be called again, no need to modify `files_to_commit` return nil end + if Helper.mac? + installed_profile = FastlaneCore::ProvisioningProfile.install(stored_profile_path, keychain_path) + end + + if params[:output_path] + FileUtils.cp(stored_profile_path, params[:output_path]) + end + Utils.fill_environment(Utils.environment_variable_name(app_identifier: app_identifier, type: prov_type, platform: params[:platform]), @@ -308,145 +348,6 @@ def fetch_provisioning_profile(params: nil, certificate_id: nil, app_identifier: return uuid end # rubocop:enable Metrics/PerceivedComplexity - - def should_force_include_all_devices(params: nil, prov_type: nil, profile: nil, keychain_path: nil) - return false unless params[:force_for_new_devices] && !params[:readonly] - - force = false - - prov_types_without_devices = [:appstore, :developer_id] - if !prov_types_without_devices.include?(prov_type) && !params[:force] - force = device_count_different?(profile: profile, keychain_path: keychain_path, platform: params[:platform].to_sym, include_mac_in_profiles: params[:include_mac_in_profiles]) - else - # App Store provisioning profiles don't contain device identifiers and - # thus shouldn't be renewed if the device count has changed. - UI.important("Warning: `force_for_new_devices` is set but is ignored for App Store & Developer ID provisioning profiles.") - UI.important("You can safely stop specifying `force_for_new_devices` when running Match for type 'appstore' or 'developer_id'.") - end - - return force - end - - def device_count_different?(profile: nil, keychain_path: nil, platform: nil, include_mac_in_profiles: false) - return false unless profile - - parsed = FastlaneCore::ProvisioningProfile.parse(profile, keychain_path) - uuid = parsed["UUID"] - - all_profiles = Spaceship::ConnectAPI::Profile.all(includes: "devices") - portal_profile = all_profiles.detect { |i| i.uuid == uuid } - - if portal_profile - profile_device_count = portal_profile.devices.count - - device_classes = - case platform - when :ios - [ - Spaceship::ConnectAPI::Device::DeviceClass::IPAD, - Spaceship::ConnectAPI::Device::DeviceClass::IPHONE, - Spaceship::ConnectAPI::Device::DeviceClass::IPOD, - Spaceship::ConnectAPI::Device::DeviceClass::APPLE_WATCH - ] - when :tvos - [ - Spaceship::ConnectAPI::Device::DeviceClass::APPLE_TV - ] - when :macos, :catalyst - [ - Spaceship::ConnectAPI::Device::DeviceClass::MAC - ] - else - [] - end - if platform == :ios && include_mac_in_profiles - device_classes += [Spaceship::ConnectAPI::Device::DeviceClass::APPLE_SILICON_MAC] - end - - devices = Spaceship::ConnectAPI::Device.all - unless device_classes.empty? - devices = devices.select do |device| - device_classes.include?(device.device_class) && device.enabled? - end - end - - portal_device_count = devices.size - - return portal_device_count != profile_device_count - end - return false - end - - def should_force_include_all_certificates(params: nil, prov_type: nil, profile: nil, keychain_path: nil) - unless params[:include_all_certificates] - if params[:force_for_new_certificates] - UI.important("You specified 'force_for_new_certificates: true', but new certificates will not be added, cause 'include_all_certificates' is 'false'") - end - return false - end - - force = false - - if params[:force_for_new_certificates] && !params[:readonly] - if prov_type == :development && !params[:force] - force = certificate_count_different?(profile: profile, keychain_path: keychain_path, platform: params[:platform].to_sym) - else - # All other (not development) provisioning profiles don't contain - # multiple certificates, thus shouldn't be renewed - # if the certificates count has changed. - UI.important("Warning: `force_for_new_certificates` is set but is ignored for non-'development' provisioning profiles.") - UI.important("You can safely stop specifying `force_for_new_certificates` when running Match for '#{prov_type}' provisioning profiles.") - end - end - - return force - end - - def certificate_count_different?(profile: nil, keychain_path: nil, platform: nil) - return false unless profile - - parsed = FastlaneCore::ProvisioningProfile.parse(profile, keychain_path) - uuid = parsed["UUID"] - - all_profiles = Spaceship::ConnectAPI::Profile.all(includes: "certificates") - portal_profile = all_profiles.detect { |i| i.uuid == uuid } - - return false unless portal_profile - - # When a certificate expires (not revoked) provisioning profile stays valid. - # And if we regenerate certificate count will not differ: - # * For portal certificates, we filter out the expired one but includes a new certificate; - # * Profile still contains an expired certificate and is valid. - # Thus, we need to check the validity of profile certificates too. - profile_certs_count = portal_profile.certificates.select(&:valid?).count - - certificate_types = - case platform - when :ios, :tvos - [ - Spaceship::ConnectAPI::Certificate::CertificateType::DEVELOPMENT, - Spaceship::ConnectAPI::Certificate::CertificateType::IOS_DEVELOPMENT - ] - when :macos, :catalyst - [ - Spaceship::ConnectAPI::Certificate::CertificateType::DEVELOPMENT, - Spaceship::ConnectAPI::Certificate::CertificateType::MAC_APP_DEVELOPMENT - ] - else - [] - end - - certificates = Spaceship::ConnectAPI::Certificate.all - unless certificate_types.empty? - certificates = certificates.select do |certificate| - certificate_types.include?(certificate.certificateType) && certificate.valid? - end - end - - portal_certs_count = certificates.size - - return portal_certs_count != profile_certs_count - end end # rubocop:enable Metrics/ClassLength end diff --git a/match/lib/match/spaceship_ensure.rb b/match/lib/match/spaceship_ensure.rb index 01085095f30..28a44d734d8 100644 --- a/match/lib/match/spaceship_ensure.rb +++ b/match/lib/match/spaceship_ensure.rb @@ -1,5 +1,6 @@ require 'spaceship' require_relative 'module' +require_relative 'portal_fetcher' module Match # Ensures the certificate and profiles are also available on App Store Connect @@ -41,8 +42,9 @@ def team_id return @team_id end - def bundle_identifier_exists(username: nil, app_identifier: nil, platform: nil) - found = Spaceship::ConnectAPI::BundleId.find(app_identifier) + def bundle_identifier_exists(username: nil, app_identifier: nil, cached_bundle_ids: nil) + search_bundle_ids = cached_bundle_ids || Match::Portal::Fetcher.bundle_ids(bundle_id_identifiers: [app_identifier]) + found = search_bundle_ids.any? { |bundle_id| bundle_id.identifier == app_identifier } return if found require 'sigh/runner' @@ -52,14 +54,17 @@ def bundle_identifier_exists(username: nil, app_identifier: nil, platform: nil) }) UI.error("An app with that bundle ID needs to exist in order to create a provisioning profile for it") UI.error("================================================================") - available_apps = Spaceship::ConnectAPI::BundleId.all.collect { |a| "#{a.identifier} (#{a.name})" } + all_bundle_ids = Match::Portal::Fetcher.bundle_ids + available_apps = all_bundle_ids.collect { |a| "#{a.identifier} (#{a.name})" } UI.message("Available apps:\n- #{available_apps.join("\n- ")}") UI.error("Make sure to run `fastlane match` with the same user and team every time.") UI.user_error!("Couldn't find bundle identifier '#{app_identifier}' for the user '#{username}'") end - def certificates_exists(username: nil, certificate_ids: []) - Spaceship::ConnectAPI::Certificate.all.each do |cert| + def certificates_exists(username: nil, certificate_ids: [], platform:, profile_type:, cached_certificates:) + certificates = cached_certificates + certificates ||= Match::Portal::Fetcher.certificates(platform: platform, profile_type: profile_type) + certificates.each do |cert| certificate_ids.delete(cert.id) end return if certificate_ids.empty? @@ -74,12 +79,11 @@ def certificates_exists(username: nil, certificate_ids: []) UI.user_error!("To reset the certificates of your Apple account, you can use the `fastlane match nuke` feature, more information on https://docs.fastlane.tools/actions/match/") end - def profile_exists(type: nil, username: nil, uuid: nil, platform: nil) - # App Store Connect API does not allow filter of profile by platform or uuid (as of 2020-07-30) - # Need to fetch all profiles and search for uuid on client side - # But we can filter provisioning profiles based on their type (this, in general way faster than getting all profiles) - filter = { profileType: Match.profile_types(type).join(",") } if type - found = Spaceship::ConnectAPI::Profile.all(filter: filter).find do |profile| + def profile_exists(profile_type: nil, name: nil, username: nil, uuid: nil, cached_profiles: nil) + profiles = cached_profiles + profiles ||= Match::Portal::Fetcher.profiles(profile_type: profile_type, name: name) + + found = profiles.find do |profile| profile.uuid == uuid end diff --git a/match/spec/portal_cache_spec.rb b/match/spec/portal_cache_spec.rb new file mode 100644 index 00000000000..f4369738a62 --- /dev/null +++ b/match/spec/portal_cache_spec.rb @@ -0,0 +1,147 @@ +require 'spaceship/client' + +describe Match do + describe Match::Portal::Cache do + let(:default_params) do + { + platform: 'ios', + type: 'development', + additional_cert_types: nil, + readonly: false, + force: false, + include_mac_in_profiles: true, + + force_for_new_devices: true, + include_all_certificates: true, + force_for_new_certificates: true + } + end + + let(:bundle_ids) { ['bundle_ID_1', 'bundle_ID_2'] } + + let(:default_sut) do + Match::Portal::Cache.build(params: default_params, bundle_id_identifiers: bundle_ids) + end + + describe "init" do + it "builds correctly with params" do + params = default_params + bundle_ids = ['bundleID'] + + cache = Match::Portal::Cache.build(params: params, bundle_id_identifiers: bundle_ids) + + expect(cache.bundle_id_identifiers).to eq(bundle_ids) + expect(cache.platform).to eq(params[:platform]) + expect(cache.profile_type).to eq('IOS_APP_DEVELOPMENT') + expect(cache.additional_cert_types).to eq(params[:additional_cert_types]) + expect(cache.needs_profiles_devices).to eq(true) + expect(cache.needs_profiles_certificate_content).to eq(false) + expect(cache.include_mac_in_profiles).to eq(params[:include_mac_in_profiles]) + end + end + + describe "bundle_ids" do + it 'caches bundle ids' do + # GIVEN + sut = default_sut + + allow(Match::Portal::Fetcher).to receive(:bundle_ids).with(bundle_id_identifiers: ['bundle_ID_1', 'bundle_ID_2']).and_return(['portal_bundle_id']).once + + # WHEN + bundle_ids = sut.bundle_ids + + # THEN + expect(bundle_ids).to eq(['portal_bundle_id']) + + # THEN used cached + expect(sut.bundle_ids).to eq(['portal_bundle_id']) + end + end + + describe "devices" do + it 'caches devices' do + # GIVEN + sut = default_sut + + allow(Match::Portal::Fetcher).to receive(:devices).with(include_mac_in_profiles: sut.include_mac_in_profiles, platform: sut.platform).and_return(['portal_device']).once + + # WHEN + devices = sut.devices + + # THEN + expect(devices).to eq(['portal_device']) + # THEN used cached + expect(sut.devices).to eq(['portal_device']) + end + end + + describe "devices" do + it 'caches profiles' do + # GIVEN + sut = default_sut + + allow(Match::Portal::Fetcher).to receive(:profiles).with(needs_profiles_certificate_content: sut.needs_profiles_certificate_content, needs_profiles_devices: sut.needs_profiles_devices, profile_type: sut.profile_type).and_return(['portal_profile_1']).once + + # WHEN + profiles = sut.profiles + + # THEN + expect(profiles).to eq(['portal_profile_1']) + # THEN used cached + expect(sut.profiles).to eq(['portal_profile_1']) + end + + it 'removes profile' do + # GIVEN + sut = default_sut + + allow(Match::Portal::Fetcher).to receive(:profiles).with(needs_profiles_certificate_content: sut.needs_profiles_certificate_content, needs_profiles_devices: sut.needs_profiles_devices, profile_type: sut.profile_type).and_return(['portal_profile_1', 'portal_profile_2']).once + + expect(sut.profiles).to eq(['portal_profile_1', 'portal_profile_2']) + + # WHEN + sut.forget_portal_profile('portal_profile_1') + + # THEN + expect(sut.profiles).to eq(['portal_profile_2']) + end + end + + describe "certificates" do + it 'caches certificates' do + # GIVEN + sut = default_sut + + allow(Match::Portal::Fetcher).to receive(:certificates).with(additional_cert_types: sut.additional_cert_types, platform: sut.platform, profile_type: sut.profile_type).and_return(['portal_certificate_1']).once + + # WHEN + certificates = sut.certificates + + # THEN + expect(certificates).to eq(['portal_certificate_1']) + # THEN used cached + expect(sut.certificates).to eq(['portal_certificate_1']) + end + + it 'resets certificates cache' do + # GIVEN + sut = default_sut + + allow(Match::Portal::Fetcher).to receive(:certificates).with(additional_cert_types: sut.additional_cert_types, platform: sut.platform, profile_type: sut.profile_type).and_return(['portal_certificate_1']).once + + certificates = sut.certificates + expect(certificates).to eq(['portal_certificate_1']) + + allow(Match::Portal::Fetcher).to receive(:certificates).with(additional_cert_types: sut.additional_cert_types, platform: sut.platform, profile_type: sut.profile_type).and_return(['portal_certificate_2']).once + + # WHEN + sut.reset_certificates + + # THEN + expect(sut.certificates).to eq(['portal_certificate_2']) + # THEN used cached + expect(sut.certificates).to eq(['portal_certificate_2']) + end + end + end +end diff --git a/match/spec/portal_fetcher_spec.rb b/match/spec/portal_fetcher_spec.rb new file mode 100644 index 00000000000..a1b031a5937 --- /dev/null +++ b/match/spec/portal_fetcher_spec.rb @@ -0,0 +1,133 @@ +require 'spaceship/client' + +describe Match do + describe Match::Portal::Fetcher do + let(:default_sut) { Match::Portal::Fetcher } + + let(:portal_bundle_id) { double("portal_bundle_id") } + let(:portal_device) { double("portal_device") } + let(:portal_certificate) { double("portal_certificate") } + + let(:portal_profile) { double("portal_profile") } + let(:portal_profile_udid) { double("portal_profile_udid") } + + let(:deviceClass) { Spaceship::ConnectAPI::Device::DeviceClass } + let(:deviceStatus) { Spaceship::ConnectAPI::Device::Status } + let(:certificateType) { Spaceship::ConnectAPI::Certificate::CertificateType } + let(:profileState) { Spaceship::ConnectAPI::Profile::ProfileState } + + let(:default_device_all_params) do + { + filter: { platform: 'IOS,UNIVERSAL', status: 'ENABLED' }, + client: anything + } + end + + let(:default_certificates_all_params) do + { + filter: { certificateType: anything } + } + end + + let(:default_profile_all_params) do + { + filter: { profileType: anything }, + includes: 'bundleId,devices,certificates' + } + end + + let(:default_bundle_id_all_params) do + { + filter: { identifier: anything } + } + end + + before do + allow(portal_device).to receive(:device_class).and_return(deviceClass::IPHONE) + allow(portal_device).to receive(:enabled?).and_return(true) + + allow(portal_certificate).to receive(:valid?).and_return(true) + + allow(portal_profile).to receive(:uuid).and_return(portal_profile_udid) + allow(portal_profile).to receive(:profile_state).and_return(profileState::ACTIVE) + allow(portal_profile).to receive(:devices).and_return([portal_device]) + allow(portal_profile).to receive(:certificates).and_return([portal_certificate]) + end + + before do + allow(Spaceship::ConnectAPI::Device).to receive(:all).with(default_device_all_params).and_return([portal_device]) + allow(Spaceship::ConnectAPI::Certificate).to receive(:all).with(default_certificates_all_params).and_return([portal_certificate]) + allow(Spaceship::ConnectAPI::Profile).to receive(:all).with(default_profile_all_params).and_return([portal_profile]) + allow(Spaceship::ConnectAPI::BundleId).to receive(:all).with(default_bundle_id_all_params).and_return([portal_bundle_id]) + end + + describe "certificates" do + it "fetches certificates" do + # GIVEN + sut = default_sut + + # WHEN + portal_certificates = sut.certificates(platform: 'ios', profile_type: 'development', additional_cert_types: nil) + + # THEN + expect(portal_certificates).to eq([portal_certificate]) + end + end + + describe "devices" do + it "fetches devices" do + # GIVEN + sut = default_sut + + # WHEN + portal_devices = sut.devices(platform: 'ios') + + # THEN + expect(portal_devices).to eq([portal_device]) + end + end + + describe "bundle ids" do + it "fetches bundle ids" do + # GIVEN + sut = default_sut + + # WHEN + portal_bundle_ids = sut.bundle_ids(bundle_id_identifiers: ['bundle_id']) + + # THEN + expect(portal_bundle_ids).to eq([portal_bundle_id]) + end + end + + describe "profiles" do + it "fetches profiles" do + # GIVEN + sut = default_sut + + # WHEN + portal_profiles = sut.profiles(profile_type: 'profile_type', needs_profiles_devices: true, needs_profiles_certificate_content: false, name: nil) + + # THEN + expect(portal_profiles).to eq([portal_profile]) + end + + it "fetches profiles with name" do + # GIVEN + sut = default_sut + + profile_params = default_profile_all_params + profile_name = 'profile name' + profile_params[:filter][:name] = profile_name + + allow(Spaceship::ConnectAPI::Profile).to receive(:all).with(profile_params).and_return([portal_profile]) + + # WHEN + portal_profiles = sut.profiles(profile_type: 'profile_type', needs_profiles_devices: true, needs_profiles_certificate_content: false, name: profile_name) + + # THEN + expect(portal_profiles).to eq([portal_profile]) + end + end + end +end diff --git a/match/spec/profile_includes_spec.rb b/match/spec/profile_includes_spec.rb new file mode 100644 index 00000000000..082e2bb5f75 --- /dev/null +++ b/match/spec/profile_includes_spec.rb @@ -0,0 +1,196 @@ +describe Match do + describe Match::ProfileIncludes do + describe "counts" do + let(:portal_profile) { double("profile") } + let(:profile_device) { double("profile_device") } + let(:profile_certificate) { double("profile_certificate") } + + before do + allow(portal_profile).to receive(:devices).and_return([profile_device]) + allow(portal_profile).to receive(:certificates).and_return([profile_certificate]) + allow(profile_device).to receive(:id).and_return(1) + allow(profile_certificate).to receive(:id).and_return(1) + end + + describe "#devices_differ?" do + it "returns false if devices are the same" do + # WHEN + devices_differ = Match::ProfileIncludes.devices_differ?(portal_profile: portal_profile, platform: 'ios', include_mac_in_profiles: true, cached_devices: [profile_device]) + + # THEN + expect(devices_differ).to be(false) + end + + it "returns true if devices differ even when the count is the same" do + # GIVEN + portal_device = double("profile_device") + allow(portal_device).to receive(:id).and_return(2) + + # WHEN + devices_differ = Match::ProfileIncludes.devices_differ?(portal_profile: portal_profile, platform: 'ios', include_mac_in_profiles: true, cached_devices: [portal_device]) + + # THEN + expect(devices_differ).to be(true) + end + + it "returns true if devices differ" do + # GIVEN + portal_device = double("profile_device") + allow(portal_device).to receive(:id).and_return(2) + + # WHEN + devices_differ = Match::ProfileIncludes.devices_differ?(portal_profile: portal_profile, platform: 'ios', include_mac_in_profiles: true, cached_devices: [portal_device, profile_device]) + + # THEN + expect(devices_differ).to be(true) + end + end + + describe "#certificates_differ?" do + it "returns false if certificates are the same" do + # WHEN + certificates_differ = Match::ProfileIncludes.certificates_differ?(portal_profile: portal_profile, platform: 'ios', cached_certificates: [profile_certificate]) + + # THEN + expect(certificates_differ).to be(false) + end + + it "returns true if certs differ even when the count is the same" do + # GIVEN + portal_cert = double("profile_device") + allow(portal_cert).to receive(:id).and_return(2) + + # WHEN + certificates_differ = Match::ProfileIncludes.certificates_differ?(portal_profile: portal_profile, platform: 'ios', cached_certificates: [portal_cert]) + + # THEN + expect(certificates_differ).to be(true) + end + + it "returns true if certs differ" do + # GIVEN + portal_cert = double("profile_device") + allow(portal_cert).to receive(:id).and_return(2) + + # WHEN + certificates_differ = Match::ProfileIncludes.certificates_differ?(portal_profile: portal_profile, platform: 'ios', cached_certificates: [profile_certificate, portal_cert]) + + # THEN + expect(certificates_differ).to be(true) + end + end + end + + describe "can's" do + let(:params) { double("params") } + + before do + allow(params).to receive(:[]).with(:type).and_return('development') + + allow(params).to receive(:[]).with(:readonly).and_return(false) + allow(params).to receive(:[]).with(:force).and_return(false) + + allow(params).to receive(:[]).with(:force_for_new_devices).and_return(true) + allow(params).to receive(:[]).with(:force_for_new_certificates).and_return(true) + allow(params).to receive(:[]).with(:include_all_certificates).and_return(true) + end + + describe "#can_force_include_all_devices?" do + it "returns true if params ok" do + # WHEN + can_include_devices = Match::ProfileIncludes.can_force_include_all_devices?(params: params) + + # THEN + expect(can_include_devices).to be(true) + end + + it "returns false if readonly" do + # GIVEN + allow(params).to receive(:[]).with(:readonly).and_return(true) + + # WHEN + can_include_devices = Match::ProfileIncludes.can_force_include_all_devices?(params: params) + + # THEN + expect(can_include_devices).to be(false) + end + + it "returns false if no force_for_new_devices" do + # GIVEN + allow(params).to receive(:[]).with(:force_for_new_devices).and_return(false) + + # WHEN + can_include_devices = Match::ProfileIncludes.can_force_include_all_devices?(params: params) + + # THEN + expect(can_include_devices).to be(false) + end + + it "returns false if type is unsutable" do + # GIVEN + allow(params).to receive(:[]).with(:type).and_return('appstore') + + # WHEN + can_include_devices = Match::ProfileIncludes.can_force_include_all_devices?(params: params) + + # THEN + expect(can_include_devices).to be(false) + end + end + + describe "#can_force_include_all_certificates?" do + it "returns true if params ok" do + # WHEN + can_include_certs = Match::ProfileIncludes.can_force_include_all_certificates?(params: params) + + # THEN + expect(can_include_certs).to be(true) + end + + it "returns false if readonly" do + # GIVEN + allow(params).to receive(:[]).with(:readonly).and_return(true) + + # WHEN + can_include_certs = Match::ProfileIncludes.can_force_include_all_certificates?(params: params) + + # THEN + expect(can_include_certs).to be(false) + end + + it "returns false if no force_for_new_devices" do + # GIVEN + allow(params).to receive(:[]).with(:force_for_new_certificates).and_return(false) + + # WHEN + can_include_certs = Match::ProfileIncludes.can_force_include_all_certificates?(params: params) + + # THEN + expect(can_include_certs).to be(false) + end + + it "returns false if no include_all_certificates" do + # GIVEN + allow(params).to receive(:[]).with(:include_all_certificates).and_return(false) + + # WHEN + can_include_certs = Match::ProfileIncludes.can_force_include_all_certificates?(params: params) + + # THEN + expect(can_include_certs).to be(false) + end + + it "returns false if type is unsutable" do + # GIVEN + allow(params).to receive(:[]).with(:type).and_return('appstore') + + # WHEN + can_include_certs = Match::ProfileIncludes.can_force_include_all_certificates?(params: params) + + # THEN + expect(can_include_certs).to be(false) + end + end + end + end +end diff --git a/match/spec/runner_spec.rb b/match/spec/runner_spec.rb index 630ca1c0827..8ab30aa37b8 100644 --- a/match/spec/runner_spec.rb +++ b/match/spec/runner_spec.rb @@ -1,12 +1,21 @@ describe Match do describe Match::Runner do let(:keychain) { 'login.keychain' } + let(:fake_cache) { double('fake_cache') } before do allow(ENV).to receive(:[]).and_call_original allow(ENV).to receive(:[]).with('MATCH_KEYCHAIN_NAME').and_return(keychain) allow(ENV).to receive(:[]).with('MATCH_KEYCHAIN_PASSWORD').and_return(nil) + allow(Match::Portal::Cache).to receive(:new).and_return(fake_cache) + allow(fake_cache).to receive(:bundle_ids).and_return(nil) + allow(fake_cache).to receive(:certificates).and_return(nil) + allow(fake_cache).to receive(:profiles).and_return(nil) + allow(fake_cache).to receive(:devices).and_return(nil) + allow(fake_cache).to receive(:portal_profile).and_return(nil) + allow(fake_cache).to receive(:reset_certificates) + # There is another test ENV.delete('FASTLANE_TEAM_ID') ENV.delete('FASTLANE_TEAM_NAME') @@ -65,6 +74,7 @@ certificate_id: "something", app_identifier: values[:app_identifier], force: false, + cache: fake_cache, working_directory: fake_storage.working_directory).and_return(profile_path) expect(FastlaneCore::ProvisioningProfile).to receive(:install).with(profile_path, keychain_path).and_return(destination) expect(fake_storage).to receive(:save_changes!).with( @@ -79,7 +89,7 @@ allow(spaceship).to receive(:team_id).and_return("") expect(Match::SpaceshipEnsure).to receive(:new).and_return(spaceship) expect(spaceship).to receive(:certificates_exists).and_return(true) - expect(spaceship).to receive(:profile_exists).and_return(true) + expect(spaceship).not_to receive(:profile_exists) expect(spaceship).to receive(:bundle_identifier_exists).and_return(true) expect(Match::Utils).to receive(:get_cert_info).and_return([["Common Name", "fastlane certificate name"]]) @@ -282,55 +292,5 @@ end end end - - describe "#device_count_different?" do - let(:profile_file) { double("profile file") } - let(:uuid) { "1234-1234-1234-1234" } - let(:parsed_profile) { { "UUID" => uuid } } - let(:profile) { double("profile") } - let(:profile_device) { double("profile_device") } - - before do - allow(profile).to receive(:uuid).and_return(uuid) - allow(profile).to receive(:devices).and_return([profile_device]) - end - - it "device is enabled" do - expect(FastlaneCore::ProvisioningProfile).to receive(:parse).and_return(parsed_profile) - expect(Spaceship::ConnectAPI::Profile).to receive(:all).and_return([profile]) - expect(Spaceship::ConnectAPI::Device).to receive(:all).and_return([profile_device]) - - expect(profile_device).to receive(:device_class).and_return(Spaceship::ConnectAPI::Device::DeviceClass::IPOD) - expect(profile_device).to receive(:enabled?).and_return(true) - - runner = Match::Runner.new - expect(runner.device_count_different?(profile: profile_file, platform: :ios)).to be(false) - end - - it "device is disabled" do - expect(FastlaneCore::ProvisioningProfile).to receive(:parse).and_return(parsed_profile) - expect(Spaceship::ConnectAPI::Profile).to receive(:all).and_return([profile]) - expect(Spaceship::ConnectAPI::Device).to receive(:all).and_return([profile_device]) - - expect(profile_device).to receive(:device_class).and_return(Spaceship::ConnectAPI::Device::DeviceClass::IPOD) - expect(profile_device).to receive(:enabled?).and_return(false) - - runner = Match::Runner.new - expect(runner.device_count_different?(profile: profile_file, platform: :ios)).to be(true) - end - - it "device is apple silicon mac" do - expect(FastlaneCore::ProvisioningProfile).to receive(:parse).twice.and_return(parsed_profile) - expect(Spaceship::ConnectAPI::Profile).to receive(:all).twice.and_return([profile]) - expect(Spaceship::ConnectAPI::Device).to receive(:all).twice.and_return([profile_device]) - - expect(profile_device).to receive(:device_class).twice.and_return(Spaceship::ConnectAPI::Device::DeviceClass::APPLE_SILICON_MAC) - expect(profile_device).to receive(:enabled?).and_return(true) - - runner = Match::Runner.new - expect(runner.device_count_different?(profile: profile_file, platform: :ios, include_mac_in_profiles: false)).to be(true) - expect(runner.device_count_different?(profile: profile_file, platform: :ios, include_mac_in_profiles: true)).to be(false) - end - end end end diff --git a/sigh/lib/sigh/module.rb b/sigh/lib/sigh/module.rb index 528b9fd2119..b708788dbe4 100644 --- a/sigh/lib/sigh/module.rb +++ b/sigh/lib/sigh/module.rb @@ -33,6 +33,104 @@ def profile_pretty_type(profile_type) "Direct" end end + + def profile_type_for_config(platform:, in_house:, config:) + profile_type = nil + + case platform.to_s + when "ios" + profile_type = Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_STORE + profile_type = Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_INHOUSE if in_house + profile_type = Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_ADHOC if config[:adhoc] + profile_type = Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_DEVELOPMENT if config[:development] + when "tvos" + profile_type = Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_STORE + profile_type = Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_INHOUSE if in_house + profile_type = Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_ADHOC if config[:adhoc] + profile_type = Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_DEVELOPMENT if config[:development] + when "macos" + profile_type = Spaceship::ConnectAPI::Profile::ProfileType::MAC_APP_STORE + profile_type = Spaceship::ConnectAPI::Profile::ProfileType::MAC_APP_INHOUSE if in_house + profile_type = Spaceship::ConnectAPI::Profile::ProfileType::MAC_APP_DEVELOPMENT if config[:development] + profile_type = Spaceship::ConnectAPI::Profile::ProfileType::MAC_APP_DIRECT if config[:developer_id] + when "catalyst" + profile_type = Spaceship::ConnectAPI::Profile::ProfileType::MAC_CATALYST_APP_STORE + profile_type = Spaceship::ConnectAPI::Profile::ProfileType::MAC_CATALYST_APP_INHOUSE if in_house + profile_type = Spaceship::ConnectAPI::Profile::ProfileType::MAC_CATALYST_APP_DEVELOPMENT if config[:development] + profile_type = Spaceship::ConnectAPI::Profile::ProfileType::MAC_CATALYST_APP_DIRECT if config[:developer_id] + end + + profile_type + end + + def profile_type_for_distribution_type(platform:, distribution_type:) + config = { distribution_type.to_sym => true } + in_house = distribution_type == "enterprise" + + self.profile_type_for_config(platform: platform, in_house: in_house, config: config) + end + + def certificate_types_for_profile_and_platform(platform:, profile_type:) + types = [] + + case platform + when 'ios', 'tvos' + if profile_type == Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_DEVELOPMENT || profile_type == Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_DEVELOPMENT + types = [ + Spaceship::ConnectAPI::Certificate::CertificateType::DEVELOPMENT, + Spaceship::ConnectAPI::Certificate::CertificateType::IOS_DEVELOPMENT + ] + elsif profile_type == Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_INHOUSE || profile_type == Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_INHOUSE + # Enterprise accounts don't have access to Apple Distribution certificates + types = [ + Spaceship::ConnectAPI::Certificate::CertificateType::IOS_DISTRIBUTION + ] + # handles case where the desired certificate type is adhoc but the account is an enterprise account + # the apple dev portal api has a weird quirk in it where if you query for distribution certificates + # for enterprise accounts, you get nothing back even if they exist. + elsif (profile_type == Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_ADHOC || profile_type == Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_ADHOC) && Spaceship::ConnectAPI.client && Spaceship::ConnectAPI.client.in_house? + # Enterprise accounts don't have access to Apple Distribution certificates + types = [ + Spaceship::ConnectAPI::Certificate::CertificateType::IOS_DISTRIBUTION + ] + else + types = [ + Spaceship::ConnectAPI::Certificate::CertificateType::DISTRIBUTION, + Spaceship::ConnectAPI::Certificate::CertificateType::IOS_DISTRIBUTION + ] + end + + when 'macos', 'catalyst' + if profile_type == Spaceship::ConnectAPI::Profile::ProfileType::MAC_APP_DEVELOPMENT || profile_type == Spaceship::ConnectAPI::Profile::ProfileType::MAC_CATALYST_APP_DEVELOPMENT + types = [ + Spaceship::ConnectAPI::Certificate::CertificateType::DEVELOPMENT, + Spaceship::ConnectAPI::Certificate::CertificateType::MAC_APP_DEVELOPMENT + ] + elsif profile_type == Spaceship::ConnectAPI::Profile::ProfileType::MAC_APP_STORE || profile_type == Spaceship::ConnectAPI::Profile::ProfileType::MAC_CATALYST_APP_STORE + types = [ + Spaceship::ConnectAPI::Certificate::CertificateType::DISTRIBUTION, + Spaceship::ConnectAPI::Certificate::CertificateType::MAC_APP_DISTRIBUTION + ] + elsif profile_type == Spaceship::ConnectAPI::Profile::ProfileType::MAC_APP_DIRECT || profile_type == Spaceship::ConnectAPI::Profile::ProfileType::MAC_CATALYST_APP_DIRECT + types = [ + Spaceship::ConnectAPI::Certificate::CertificateType::DEVELOPER_ID_APPLICATION, + Spaceship::ConnectAPI::Certificate::CertificateType::DEVELOPER_ID_APPLICATION_G2 + ] + elsif profile_type == Spaceship::ConnectAPI::Profile::ProfileType::MAC_APP_INHOUSE || profile_type == Spaceship::ConnectAPI::Profile::ProfileType::MAC_CATALYST_APP_INHOUSE + # Enterprise accounts don't have access to Apple Distribution certificates + types = [ + Spaceship::ConnectAPI::Certificate::CertificateType::MAC_APP_DISTRIBUTION + ] + else + types = [ + Spaceship::ConnectAPI::Certificate::CertificateType::DISTRIBUTION, + Spaceship::ConnectAPI::Certificate::CertificateType::MAC_APP_DISTRIBUTION + ] + end + end + + types + end end Helper = FastlaneCore::Helper # you gotta love Ruby: Helper.* should use the Helper class contained in FastlaneCore diff --git a/sigh/lib/sigh/options.rb b/sigh/lib/sigh/options.rb index dd08f9f6e27..8728fceb12d 100644 --- a/sigh/lib/sigh/options.rb +++ b/sigh/lib/sigh/options.rb @@ -1,6 +1,10 @@ require 'fastlane_core/configuration/configuration' require 'credentials_manager/appfile_config' require_relative 'module' +require 'spaceship/connect_api/models/device' +require 'spaceship/connect_api/models/certificate' +require 'spaceship/connect_api/models/bundle_id' +require 'spaceship/connect_api/models/profile' module Sigh class Options @@ -192,7 +196,57 @@ def self.available_options description: "Should the command fail if it was about to create a duplicate of an existing provisioning profile. It can happen due to issues on Apple Developer Portal, when profile to be recreated was not properly deleted first", optional: true, is_string: false, - default_value: false) + default_value: false), + + # Cache + FastlaneCore::ConfigItem.new(key: :cached_certificates, + description: "A list of cached certificates", + optional: true, + is_string: false, + default_value: nil, + verify_block: proc do |value| + if !value.kind_of?(Array) || + value.empty? || + !value.all?(Spaceship::ConnectAPI::Certificate) + UI.user_error!("cached_certificates parameter must be a non-empty array of Spaceship::ConnectAPI::Certificate") unless value.kind_of?(Array) + end + end), + FastlaneCore::ConfigItem.new(key: :cached_devices, + description: "A list of cached devices", + optional: true, + is_string: false, + default_value: nil, + verify_block: proc do |value| + if !value.kind_of?(Array) || + value.empty? || + !value.all?(Spaceship::ConnectAPI::Device) + UI.user_error!("cached_devices parameter must be a non-empty array of Spaceship::ConnectAPI::Device") + end + end), + FastlaneCore::ConfigItem.new(key: :cached_bundle_ids, + description: "A list of cached bundle ids", + optional: true, + is_string: false, + default_value: nil, + verify_block: proc do |value| + if !value.kind_of?(Array) || + value.empty? || + !value.all?(Spaceship::ConnectAPI::BundleId) + UI.user_error!("cached_bundle_ids parameter must be a non-empty array of Spaceship::ConnectAPI::BundleId") + end + end), + FastlaneCore::ConfigItem.new(key: :cached_profiles, + description: "A list of cached bundle ids", + optional: true, + is_string: false, + default_value: nil, + verify_block: proc do |value| + if !value.kind_of?(Array) || + value.empty? || + !value.all?(Spaceship::ConnectAPI::Profile) + UI.user_error!("cached_profiles parameter must be a non-empty array of Spaceship::ConnectAPI::Profile") + end + end) ] end end diff --git a/sigh/lib/sigh/runner.rb b/sigh/lib/sigh/runner.rb index 3ffaa824e48..b5b1c0b7332 100644 --- a/sigh/lib/sigh/runner.rb +++ b/sigh/lib/sigh/runner.rb @@ -14,7 +14,7 @@ class Runner # returns the path the newly created provisioning profile (in /tmp usually) def run FastlaneCore::PrintTable.print_values(config: Sigh.config, - hide_keys: [:output_path], + hide_keys: [:output_path, :cached_certificates, :cached_devices, :cached_bundle_ids, :cached_profiles], title: "Summary for sigh #{Fastlane::VERSION}") if (api_token = Spaceship::ConnectAPI::Token.from(hash: Sigh.config[:api_key], filepath: Sigh.config[:api_key_path])) @@ -69,28 +69,7 @@ def run def profile_type return @profile_type if @profile_type - case Sigh.config[:platform] - when "ios" - @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_STORE - @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_INHOUSE if Spaceship::ConnectAPI.client.in_house? - @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_ADHOC if Sigh.config[:adhoc] - @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_DEVELOPMENT if Sigh.config[:development] - when "tvos" - @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_STORE - @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_INHOUSE if Spaceship::ConnectAPI.client.in_house? - @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_ADHOC if Sigh.config[:adhoc] - @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_DEVELOPMENT if Sigh.config[:development] - when "macos" - @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::MAC_APP_STORE - @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::MAC_APP_INHOUSE if Spaceship::ConnectAPI.client.in_house? - @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::MAC_APP_DEVELOPMENT if Sigh.config[:development] - @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::MAC_APP_DIRECT if Sigh.config[:developer_id] - when "catalyst" - @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::MAC_CATALYST_APP_STORE - @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::MAC_CATALYST_APP_INHOUSE if Spaceship::ConnectAPI.client.in_house? - @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::MAC_CATALYST_APP_DEVELOPMENT if Sigh.config[:development] - @profile_type = Spaceship::ConnectAPI::Profile::ProfileType::MAC_CATALYST_APP_DIRECT if Sigh.config[:developer_id] - end + @profile_type = Sigh.profile_type_for_config(platform: Sigh.config[:platform], in_house: Spaceship::ConnectAPI.client.in_house?, config: Sigh.config) @profile_type end @@ -99,9 +78,19 @@ def profile_type def fetch_profiles UI.message("Fetching profiles...") - # Filtering on 'profileType' seems to be undocumented as of 2020-07-30 - # but works on both web session and official API - results = Spaceship::ConnectAPI::Profile.all(filter: { profileType: profile_type }, includes: "bundleId,certificates").select do |profile| + filter = { profileType: profile_type } + # We can greatly speed up the search by filtering on the provisioning profile name + filter[:name] = Sigh.config[:provisioning_name] if Sigh.config[:provisioning_name].to_s.length > 0 + + includes = 'bundleId' + + unless Sigh.config[:skip_certificate_verification] || Sigh.config[:include_all_certificates] + includes += ',certificates' + end + + results = Sigh.config[:cached_profiles] + results ||= Spaceship::ConnectAPI::Profile.all(filter: filter, includes: includes) + results.select! do |profile| profile.bundle_id.identifier == Sigh.config[:app_identifier] end @@ -166,7 +155,9 @@ def create_profile! name = Sigh.config[:provisioning_name] || [app_identifier, profile_type_pretty_type].join(' ') unless Sigh.config[:skip_fetch_profiles] - profile = Spaceship::ConnectAPI::Profile.all.find { |p| p.name == name } + # We can greatly speed up the search by filtering on the provisioning profile name + # It seems that there's no way to search for exact match using the API, so we'll need to run additional checks afterwards + profile = Spaceship::ConnectAPI::Profile.all(filter: { name: name }).find { |p| p.name == name } if profile UI.user_error!("The name '#{name}' is already taken, and fail_on_name_taken is true") if Sigh.config[:fail_on_name_taken] UI.error("The name '#{name}' is already taken, using another one.") @@ -174,7 +165,9 @@ def create_profile! end end - bundle_id = Spaceship::ConnectAPI::BundleId.find(app_identifier) + bundle_ids = Sigh.config[:cached_bundle_ids] + bundle_id = bundle_ids.detect { |e| e.identifier == app_identifier } if bundle_ids + bundle_id ||= Spaceship::ConnectAPI::BundleId.find(app_identifier) unless bundle_id UI.user_error!("Could not find App with App Identifier '#{Sigh.config[:app_identifier]}'") end @@ -207,67 +200,15 @@ def fetch_certificates(certificate_types) filter = { certificateType: certificate_types.join(',') } - return Spaceship::ConnectAPI::Certificate.all(filter: filter) + + certificates = Sigh.config[:cached_certificates] + certificates ||= Spaceship::ConnectAPI::Certificate.all(filter: filter) + + return certificates end def certificates_for_profile_and_platform - types = [] - - case Sigh.config[:platform].to_s - when 'ios', 'tvos' - if profile_type == Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_DEVELOPMENT || profile_type == Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_DEVELOPMENT - types = [ - Spaceship::ConnectAPI::Certificate::CertificateType::DEVELOPMENT, - Spaceship::ConnectAPI::Certificate::CertificateType::IOS_DEVELOPMENT - ] - elsif profile_type == Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_INHOUSE || profile_type == Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_INHOUSE - # Enterprise accounts don't have access to Apple Distribution certificates - types = [ - Spaceship::ConnectAPI::Certificate::CertificateType::IOS_DISTRIBUTION - ] - # handles case where the desired certificate type is adhoc but the account is an enterprise account - # the apple dev portal api has a weird quirk in it where if you query for distribution certificates - # for enterprise accounts, you get nothing back even if they exist. - elsif (profile_type == Spaceship::ConnectAPI::Profile::ProfileType::IOS_APP_ADHOC || profile_type == Spaceship::ConnectAPI::Profile::ProfileType::TVOS_APP_ADHOC) && Spaceship::ConnectAPI.client && Spaceship::ConnectAPI.client.in_house? - # Enterprise accounts don't have access to Apple Distribution certificates - types = [ - Spaceship::ConnectAPI::Certificate::CertificateType::IOS_DISTRIBUTION - ] - else - types = [ - Spaceship::ConnectAPI::Certificate::CertificateType::DISTRIBUTION, - Spaceship::ConnectAPI::Certificate::CertificateType::IOS_DISTRIBUTION - ] - end - - when 'macos', 'catalyst' - if profile_type == Spaceship::ConnectAPI::Profile::ProfileType::MAC_APP_DEVELOPMENT || profile_type == Spaceship::ConnectAPI::Profile::ProfileType::MAC_CATALYST_APP_DEVELOPMENT - types = [ - Spaceship::ConnectAPI::Certificate::CertificateType::DEVELOPMENT, - Spaceship::ConnectAPI::Certificate::CertificateType::MAC_APP_DEVELOPMENT - ] - elsif profile_type == Spaceship::ConnectAPI::Profile::ProfileType::MAC_APP_STORE || profile_type == Spaceship::ConnectAPI::Profile::ProfileType::MAC_CATALYST_APP_STORE - types = [ - Spaceship::ConnectAPI::Certificate::CertificateType::DISTRIBUTION, - Spaceship::ConnectAPI::Certificate::CertificateType::MAC_APP_DISTRIBUTION - ] - elsif profile_type == Spaceship::ConnectAPI::Profile::ProfileType::MAC_APP_DIRECT || profile_type == Spaceship::ConnectAPI::Profile::ProfileType::MAC_CATALYST_APP_DIRECT - types = [ - Spaceship::ConnectAPI::Certificate::CertificateType::DEVELOPER_ID_APPLICATION, - Spaceship::ConnectAPI::Certificate::CertificateType::DEVELOPER_ID_APPLICATION_G2 - ] - elsif profile_type == Spaceship::ConnectAPI::Profile::ProfileType::MAC_APP_INHOUSE || profile_type == Spaceship::ConnectAPI::Profile::ProfileType::MAC_CATALYST_APP_INHOUSE - # Enterprise accounts don't have access to Apple Distribution certificates - types = [ - Spaceship::ConnectAPI::Certificate::CertificateType::MAC_APP_DISTRIBUTION - ] - else - types = [ - Spaceship::ConnectAPI::Certificate::CertificateType::DISTRIBUTION, - Spaceship::ConnectAPI::Certificate::CertificateType::MAC_APP_DISTRIBUTION - ] - end - end + types = Sigh.certificate_types_for_profile_and_platform(platform: Sigh.config[:platform], profile_type: profile_type) fetch_certificates(types) end @@ -276,30 +217,13 @@ def devices_to_use # Only use devices if development or adhoc return [] if !Sigh.config[:development] && !Sigh.config[:adhoc] - device_classes = case Sigh.config[:platform].to_s - when 'ios' - [ - Spaceship::ConnectAPI::Device::DeviceClass::APPLE_WATCH, - Spaceship::ConnectAPI::Device::DeviceClass::IPAD, - Spaceship::ConnectAPI::Device::DeviceClass::IPHONE, - Spaceship::ConnectAPI::Device::DeviceClass::IPOD - ] - when 'tvos' - [Spaceship::ConnectAPI::Device::DeviceClass::APPLE_TV] - when 'macos', 'catalyst' - [Spaceship::ConnectAPI::Device::DeviceClass::MAC] - end - if Sigh.config[:platform].to_s == 'ios' && Sigh.config[:include_mac_in_profiles] - device_classes += [Spaceship::ConnectAPI::Device::DeviceClass::APPLE_SILICON_MAC] - end - if Spaceship::ConnectAPI.token - return Spaceship::ConnectAPI::Device.all.select do |device| - device_classes.include?(device.device_class) - end - else - filter = { deviceClass: device_classes.join(",") } - return Spaceship::ConnectAPI::Device.all(filter: filter) - end + devices = Sigh.config[:cached_devices] + devices ||= Spaceship::ConnectAPI::Device.devices_for_platform( + platform: Sigh.config[:platform], + include_mac_in_profiles: Sigh.config[:include_mac_in_profiles] + ) + + return devices end # Certificate to use based on the current distribution mode diff --git a/sigh/spec/runner_spec.rb b/sigh/spec/runner_spec.rb index 312d43a9140..5731bd4e71f 100644 --- a/sigh/spec/runner_spec.rb +++ b/sigh/spec/runner_spec.rb @@ -125,7 +125,7 @@ options = {} Sigh.config = FastlaneCore::Configuration.create(Sigh::Options.available_options, options) - expect(Spaceship::ConnectAPI::Device).not_to(receive(:all)) + expect(Spaceship::ConnectAPI::Device).not_to(receive(:devices_for_platform)) devices = fake_runner.devices_to_use expect(devices.size).to eq(0) @@ -135,7 +135,7 @@ options = { developer_id: true } Sigh.config = FastlaneCore::Configuration.create(Sigh::Options.available_options, options) - expect(Spaceship::ConnectAPI::Device).not_to(receive(:all)) + expect(Spaceship::ConnectAPI::Device).not_to(receive(:devices_for_platform)) devices = fake_runner.devices_to_use expect(devices.size).to eq(0) @@ -145,7 +145,7 @@ options = { development: true } Sigh.config = FastlaneCore::Configuration.create(Sigh::Options.available_options, options) - expect(Spaceship::ConnectAPI::Device).to receive(:all).and_return(["device"]) + expect(Spaceship::ConnectAPI::Device).to receive(:devices_for_platform).and_return(["device"]) devices = fake_runner.devices_to_use expect(devices.size).to eq(1) @@ -155,7 +155,7 @@ options = { development: true, include_mac_in_profiles: true } Sigh.config = FastlaneCore::Configuration.create(Sigh::Options.available_options, options) - expect(Spaceship::ConnectAPI::Device).to receive(:all).and_return(["ios_device", "as_device"]) + expect(Spaceship::ConnectAPI::Device).to receive(:devices_for_platform).and_return(["ios_device", "as_device"]) devices = fake_runner.devices_to_use expect(devices.size).to eq(2) @@ -165,7 +165,7 @@ options = { adhoc: true } Sigh.config = FastlaneCore::Configuration.create(Sigh::Options.available_options, options) - expect(Spaceship::ConnectAPI::Device).to receive(:all).and_return(["device"]) + expect(Spaceship::ConnectAPI::Device).to receive(:devices_for_platform).and_return(["device"]) devices = fake_runner.devices_to_use expect(devices.size).to eq(1) diff --git a/sigh/spec/spec_helper.rb b/sigh/spec/spec_helper.rb index 28561a50667..52e944c0de3 100644 --- a/sigh/spec/spec_helper.rb +++ b/sigh/spec/spec_helper.rb @@ -15,7 +15,7 @@ def sigh_stub_spaceship_connect(inhouse: false, create_profile_app_identifier: n device = "device" allow(device).to receive(:id).and_return(1) - allow(Spaceship::ConnectAPI::Device).to receive(:all).and_return([device]) + allow(Spaceship::ConnectAPI::Device).to receive(:devices_for_platform).and_return([device]) bundle_ids = all_app_identifiers.map do |id| Spaceship::ConnectAPI::BundleId.new("123", { @@ -65,8 +65,10 @@ def sigh_stub_spaceship_connect(inhouse: false, create_profile_app_identifier: n profile end end - allow(Spaceship::ConnectAPI::Profile).to receive(:all).with(anything).and_return(profiles) allow(Spaceship::ConnectAPI::Profile).to receive(:all).and_return(profiles) + profiles.each do |profile| + allow(Spaceship::ConnectAPI::Profile).to receive(:all).with(filter: { name: profile.name }).and_return([profile]) + end # Stubs production to only receive certs certs = [Spaceship.certificate.production] diff --git a/spaceship/lib/spaceship/connect_api.rb b/spaceship/lib/spaceship/connect_api.rb index b69af3fba65..4a3d09b9fee 100644 --- a/spaceship/lib/spaceship/connect_api.rb +++ b/spaceship/lib/spaceship/connect_api.rb @@ -76,6 +76,8 @@ module Spaceship class ConnectAPI + MAX_OBJECTS_PER_PAGE_LIMIT = 200 + # Defined in the App Store Connect API docs: # https://developer.apple.com/documentation/appstoreconnectapi/platform # diff --git a/spaceship/lib/spaceship/connect_api/models/bundle_id.rb b/spaceship/lib/spaceship/connect_api/models/bundle_id.rb index ea9f61a5aaa..cc6844680ab 100644 --- a/spaceship/lib/spaceship/connect_api/models/bundle_id.rb +++ b/spaceship/lib/spaceship/connect_api/models/bundle_id.rb @@ -1,4 +1,4 @@ -require_relative '../model' +require_relative '../../connect_api' require_relative './bundle_id_capability' module Spaceship class ConnectAPI @@ -39,7 +39,7 @@ def supports_catalyst? # API # - def self.all(client: nil, filter: {}, includes: nil, fields: nil, limit: nil, sort: nil) + def self.all(client: nil, filter: {}, includes: nil, fields: nil, limit: Spaceship::ConnectAPI::MAX_OBJECTS_PER_PAGE_LIMIT, sort: nil) client ||= Spaceship::ConnectAPI resps = client.get_bundle_ids(filter: filter, includes: includes, fields: fields, limit: nil, sort: nil).all_pages return resps.flat_map(&:to_models) diff --git a/spaceship/lib/spaceship/connect_api/models/certificate.rb b/spaceship/lib/spaceship/connect_api/models/certificate.rb index 2545f6c2340..dfeaa44f0d2 100644 --- a/spaceship/lib/spaceship/connect_api/models/certificate.rb +++ b/spaceship/lib/spaceship/connect_api/models/certificate.rb @@ -1,4 +1,4 @@ -require_relative '../model' +require_relative '../../connect_api' require 'openssl' @@ -79,7 +79,7 @@ def self.create_certificate_signing_request # API # - def self.all(client: nil, filter: {}, includes: nil, fields: nil, limit: nil, sort: nil) + def self.all(client: nil, filter: {}, includes: nil, fields: nil, limit: Spaceship::ConnectAPI::MAX_OBJECTS_PER_PAGE_LIMIT, sort: nil) client ||= Spaceship::ConnectAPI resps = client.get_certificates(filter: filter, includes: includes, fields: fields, limit: limit, sort: sort).all_pages return resps.flat_map(&:to_models) diff --git a/spaceship/lib/spaceship/connect_api/models/device.rb b/spaceship/lib/spaceship/connect_api/models/device.rb index 8f2eadaaf90..49a816bbd6c 100644 --- a/spaceship/lib/spaceship/connect_api/models/device.rb +++ b/spaceship/lib/spaceship/connect_api/models/device.rb @@ -1,4 +1,5 @@ -require_relative '../model' +require_relative '../../connect_api' + module Spaceship class ConnectAPI class Device @@ -50,13 +51,91 @@ def enabled? # # API # - - def self.all(client: nil, filter: {}, includes: nil, fields: nil, limit: nil, sort: nil) + def self.all(client: nil, filter: {}, includes: nil, fields: nil, limit: Spaceship::ConnectAPI::MAX_OBJECTS_PER_PAGE_LIMIT, sort: nil) client ||= Spaceship::ConnectAPI resps = client.get_devices(filter: filter, includes: includes, fields: fields, limit: limit, sort: sort).all_pages return resps.flat_map(&:to_models) end + # @param platform [String] The provisioning profile's platform (i.e. ios, tvos, macos, catalyst). + # @param include_mac_in_profiles [Bool] Whether to include macs in iOS provisioning profiles. false by default. + # @param client [ConnectAPI] ConnectAPI client. + # @return (Device) List of enabled devices. + def self.devices_for_platform(platform: nil, include_mac_in_profiles: false, client: nil) + platform = platform.to_sym + include_mac_in_profiles &&= platform == :ios + + device_platform = case platform + when :osx, :macos, :mac + Spaceship::ConnectAPI::Platform::MAC_OS + when :ios + Spaceship::ConnectAPI::Platform::IOS + when :catalyst + Spaceship::ConnectAPI::Platform::MAC_OS + end + + device_platforms = [ + device_platform, + 'UNIVERSAL' # Universal Bundle ID platform is undocumented as of Oct 4, 2023. + ] + + device_classes = + case platform + when :ios + [ + Spaceship::ConnectAPI::Device::DeviceClass::IPAD, + Spaceship::ConnectAPI::Device::DeviceClass::IPHONE, + Spaceship::ConnectAPI::Device::DeviceClass::IPOD, + Spaceship::ConnectAPI::Device::DeviceClass::APPLE_WATCH + ] + when :tvos + [ + Spaceship::ConnectAPI::Device::DeviceClass::APPLE_TV + ] + when :macos, :catalyst + [ + Spaceship::ConnectAPI::Device::DeviceClass::MAC + ] + else + [] + end + + if include_mac_in_profiles + device_classes << Spaceship::ConnectAPI::Device::DeviceClass::APPLE_SILICON_MAC + device_platforms << Spaceship::ConnectAPI::Platform::MAC_OS + end + + filter = { + status: Spaceship::ConnectAPI::Device::Status::ENABLED, + platform: device_platforms.uniq.join(',') + } + + devices = Spaceship::ConnectAPI::Device.all( + client: client, + filter: filter + ) + + unless device_classes.empty? + devices.select! do |device| + # App Store Connect API return MAC in device_class instead of APPLE_SILICON_MAC for Silicon Macs. + # The difference between old MAC and APPLE_SILICON_MAC is provisioning uuid. + # Intel-based provisioning UUID: 01234567-89AB-CDEF-0123-456789ABCDEF. + # arm64-based provisioning UUID: 01234567-89ABCDEF12345678. + # Workaround is to include macs having: + # * 25 chars length and only one hyphen in provisioning UUID. + if include_mac_in_profiles && + device.device_class == Spaceship::ConnectAPI::Device::DeviceClass::MAC + + next device.udid.length == 25 && device.udid.count('-') == 1 + end + + device_classes.include?(device.device_class) + end + end + + devices + end + # @param client [ConnectAPI] ConnectAPI client. # @param platform [String] The platform of the device. # @param include_disabled [Bool] Whether to include disable devices. false by default. diff --git a/spaceship/lib/spaceship/connect_api/models/profile.rb b/spaceship/lib/spaceship/connect_api/models/profile.rb index 63a52d048f8..59d661bc611 100644 --- a/spaceship/lib/spaceship/connect_api/models/profile.rb +++ b/spaceship/lib/spaceship/connect_api/models/profile.rb @@ -1,4 +1,5 @@ -require_relative '../model' +require_relative '../../connect_api' + module Spaceship class ConnectAPI class Profile @@ -70,7 +71,7 @@ def valid? # API # - def self.all(client: nil, filter: {}, includes: nil, fields: nil, limit: nil, sort: nil) + def self.all(client: nil, filter: {}, includes: nil, fields: nil, limit: Spaceship::ConnectAPI::MAX_OBJECTS_PER_PAGE_LIMIT, sort: nil) client ||= Spaceship::ConnectAPI resps = client.get_profiles(filter: filter, includes: includes, fields: fields, limit: limit, sort: sort).all_pages return resps.flat_map(&:to_models)