diff --git a/.circleci/config.yml b/.circleci/config.yml index 1aa9956e8df..6dc39cb3371 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,6 +1,8 @@ --- version: 2.1 +orbs: + shellcheck: circleci/shellcheck@2.2.2 # brew install shellcheck stopped working so using this aliases: # common - cache @@ -94,7 +96,6 @@ jobs: name: Setup Build command: | mkdir -p ~/test-reports - brew install shellcheck - *bundle_install - *cache_save_bundler - run: @@ -178,7 +179,7 @@ jobs: name: Setup Build command: | mkdir -p ~/test-reports - brew install shellcheck + brew update # Needed because this lane uses "brew bundle" and CircleCI's brew install is too old for that - *bundle_install - *cache_save_bundler - run: bundle exec fastlane generate_swift_api diff --git a/fastlane_core/lib/fastlane_core/build_watcher.rb b/fastlane_core/lib/fastlane_core/build_watcher.rb index 7be599ec862..111935dca7d 100644 --- a/fastlane_core/lib/fastlane_core/build_watcher.rb +++ b/fastlane_core/lib/fastlane_core/build_watcher.rb @@ -4,6 +4,8 @@ module FastlaneCore class BuildWatcher + VersionMatches = Struct.new(:version, :builds) + class << self # @return The build we waited for. This method will always return a build def wait_for_build_processing_to_be_complete(app_id: nil, platform: nil, train_version: nil, app_version: nil, build_version: nil, poll_interval: 10, strict_build_watch: false, return_when_build_appears: false, return_spaceship_testflight_build: true, select_latest: false) @@ -23,7 +25,7 @@ def wait_for_build_processing_to_be_complete(app_id: nil, platform: nil, train_v showed_info = false loop do - matched_build = matching_build(watched_app_version: app_version, watched_build_version: build_version, app_id: app_id, platform: platform, select_latest: select_latest) + matched_build, app_version_queried = matching_build(watched_app_version: app_version, watched_build_version: build_version, app_id: app_id, platform: platform, select_latest: select_latest) if matched_build.nil? && !showed_info UI.important("Read more information on why this build isn't showing up yet - https://github.com/fastlane/fastlane/issues/14997") @@ -37,6 +39,13 @@ def wait_for_build_processing_to_be_complete(app_id: nil, platform: nil, train_v # having a build resource appear in AppStoreConnect (matched_build) may be enough (i.e. setting a changelog) # so here we may choose to skip the full processing of the build if return_when_build_appears is true if matched_build && (return_when_build_appears || matched_build.processed?) + + if !app_version.nil? && app_version != app_version_queried + UI.important("App version is #{app_version} but build was found while querying #{app_version_queried}") + UI.important("This shouldn't be an issue as Apple sees #{app_version} and #{app_version_queried} as equal") + UI.important("See docs for more info - https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/20001431-102364") + end + if return_spaceship_testflight_build return matched_build.to_testflight_build else @@ -60,15 +69,31 @@ def matching_build(watched_app_version: nil, watched_build_version: nil, app_id: watched_app_version = remove_version_leading_zeros(version: watched_app_version) watched_build_version = remove_version_leading_zeros(version: watched_build_version) - matched_builds = Spaceship::ConnectAPI::Build.all( - app_id: app_id, - version: watched_app_version, - build_number: watched_build_version, - platform: platform - ) + # App Store Connect will allow users to upload X.Y is the same as X.Y.0 and treat them as the same version + # However, only the first uploaded version format will be the one that is queryable + # This could lead to BuildWatcher never finding X.Y.0 if X.Y was upoaded first as X.Y will only yield results + # + # This will add an additional request to search for both X.Y and X.Y.0 but + # will give preference to the version format specified passed in + watched_app_version_alternate = alternate_version(watched_app_version) + versions = [watched_app_version, watched_app_version_alternate].compact + + version_matches = versions.map do |version| + match = VersionMatches.new + match.version = version + match.builds = Spaceship::ConnectAPI::Build.all( + app_id: app_id, + version: version, + build_number: watched_build_version, + platform: platform + ) + + match + end.flatten # Raise error if more than 1 build is returned # This should never happen but need to inform the user if it does + matched_builds = version_matches.map(&:builds).flatten if matched_builds.size > 1 && !select_latest error_builds = matched_builds.map do |build| "#{build.app_version}(#{build.version}) for #{build.platform} - #{build.processing_state}" @@ -77,9 +102,25 @@ def matching_build(watched_app_version: nil, watched_build_version: nil, app_id: UI.crash!(error_message) end - matched_build = matched_builds.first + version_match = version_matches.reject do |match| + match.builds.empty? + end.first + matched_build = version_match&.builds&.first + + return matched_build, version_match&.version + end + + def alternate_version(version) + return nil if version.nil? + + version_info = Gem::Version.new(version) + if version_info.segments.size == 3 && version_info.segments[2] == 0 + return version_info.segments[0..1].join(".") + elsif version_info.segments.size == 2 + return "#{version}.0" + end - return matched_build + return nil end def report_status(build: nil) diff --git a/fastlane_core/spec/build_watcher_spec.rb b/fastlane_core/spec/build_watcher_spec.rb index 1417b7f55cd..4c21cc6c678 100644 --- a/fastlane_core/spec/build_watcher_spec.rb +++ b/fastlane_core/spec/build_watcher_spec.rb @@ -23,6 +23,13 @@ let(:mock_base_api_client) { "fake api base client" } + let(:options_1_0) do + { app_id: 'some-app-id', version: '1.0', build_number: '1', platform: 'IOS' } + end + let(:options_1_0_0) do + { app_id: 'some-app-id', version: '1.0.0', build_number: '1', platform: 'IOS' } + end + before(:each) do allow(Spaceship::ConnectAPI::TestFlight::Client).to receive(:instance).and_return(mock_base_api_client) end @@ -57,9 +64,11 @@ end it 'waits when a build is still processing' do - expect(Spaceship::ConnectAPI::Build).to receive(:all).and_return([processing_build]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0).and_return([processing_build]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_0).and_return([]) expect(FastlaneCore::BuildWatcher).to receive(:sleep) - expect(Spaceship::ConnectAPI::Build).to receive(:all).and_return([ready_build]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0).and_return([ready_build]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_0).and_return([]) expect(UI).to receive(:message).with("Waiting for processing on... app_id: some-app-id, app_version: #{ready_build.app_version}, build_version: #{ready_build.version}, platform: #{ready_build.platform}") expect(UI).to receive(:message).with("Waiting for App Store Connect to finish processing the new build (1.0 - 1) for #{ready_build.platform}") @@ -70,9 +79,11 @@ end it 'waits when the build disappears' do - expect(Spaceship::ConnectAPI::Build).to receive(:all).and_return([]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0).and_return([]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_0).and_return([]) expect(FastlaneCore::BuildWatcher).to receive(:sleep) - expect(Spaceship::ConnectAPI::Build).to receive(:all).and_return([ready_build]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0).and_return([ready_build]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_0).and_return([]) expect(UI).to receive(:message).with("Waiting for processing on... app_id: some-app-id, app_version: #{ready_build.app_version}, build_version: #{ready_build.version}, platform: #{ready_build.platform}") expect(UI).to receive(:message).with("Waiting for the build to show up in the build list - this may take a few minutes (check your email for processing issues if this continues)") @@ -83,7 +94,8 @@ end it 'watches the latest build when no builds are processing' do - expect(Spaceship::ConnectAPI::Build).to receive(:all).and_return([ready_build]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0).and_return([ready_build]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_0).and_return([]) expect(UI).to receive(:success).with("Successfully finished processing the build #{ready_build.app_version} - #{ready_build.version} for #{ready_build.platform}") found_build = FastlaneCore::BuildWatcher.wait_for_build_processing_to_be_complete(app_id: 'some-app-id', platform: :ios, train_version: '1.0', build_version: '1', return_spaceship_testflight_build: false) @@ -94,9 +106,11 @@ it 'raises error when multiple builds found' do builds = [ready_build, ready_build] - expect(Spaceship::ConnectAPI::Build).to receive(:all).and_return([]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0).and_return([]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_0).and_return([]) expect(FastlaneCore::BuildWatcher).to receive(:sleep) - expect(Spaceship::ConnectAPI::Build).to receive(:all).and_return(builds) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0).and_return(builds) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_0).and_return([]) expect(UI).to receive(:message).with("Waiting for processing on... app_id: some-app-id, app_version: #{ready_build.app_version}, build_version: #{ready_build.version}, platform: #{ready_build.platform}") expect(UI).to receive(:message).with("Waiting for the build to show up in the build list - this may take a few minutes (check your email for processing issues if this continues)") @@ -109,9 +123,11 @@ end it 'sleeps 10 seconds by default' do - expect(Spaceship::ConnectAPI::Build).to receive(:all).and_return([processing_build]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0).and_return([processing_build]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_0).and_return([]) expect(FastlaneCore::BuildWatcher).to receive(:sleep).with(10) - expect(Spaceship::ConnectAPI::Build).to receive(:all).and_return([ready_build]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0).and_return([ready_build]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_0).and_return([]) allow(UI).to receive(:message) allow(UI).to receive(:success) @@ -119,13 +135,156 @@ end it 'sleeps for the amount of time specified in poll_interval' do - expect(Spaceship::ConnectAPI::Build).to receive(:all).and_return([processing_build]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0).and_return([processing_build]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_0).and_return([]) expect(FastlaneCore::BuildWatcher).to receive(:sleep).with(123) - expect(Spaceship::ConnectAPI::Build).to receive(:all).and_return([ready_build]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0).and_return([ready_build]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_0).and_return([]) allow(UI).to receive(:message) allow(UI).to receive(:success) found_build = FastlaneCore::BuildWatcher.wait_for_build_processing_to_be_complete(app_id: 'some-app-id', platform: :ios, train_version: '1.0', build_version: '1', poll_interval: 123, return_spaceship_testflight_build: false) end + + describe 'alternate versions' do + describe '1.0 with 1.0.0 alternate' do + it 'specific version returns one with alternate returns none' do + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0).and_return([processing_build]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_0).and_return([]) + expect(FastlaneCore::BuildWatcher).to receive(:sleep) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0).and_return([ready_build]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_0).and_return([]) + + expect(UI).to receive(:message).with("Waiting for processing on... app_id: some-app-id, app_version: #{ready_build.app_version}, build_version: #{ready_build.version}, platform: #{ready_build.platform}") + expect(UI).to receive(:message).with("Waiting for App Store Connect to finish processing the new build (1.0 - 1) for #{ready_build.platform}") + expect(UI).to receive(:success).with("Successfully finished processing the build #{ready_build.app_version} - #{ready_build.version} for #{ready_build.platform}") + found_build = FastlaneCore::BuildWatcher.wait_for_build_processing_to_be_complete(app_id: 'some-app-id', platform: :ios, train_version: '1.0', build_version: '1', return_spaceship_testflight_build: false) + + expect(found_build).to eq(ready_build) + end + + it 'specific version returns non but alternate returns one' do + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0).and_return([]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_0).and_return([processing_build]) + expect(FastlaneCore::BuildWatcher).to receive(:sleep) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0).and_return([]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_0).and_return([ready_build]) + + expect(UI).to receive(:message).with("Waiting for processing on... app_id: some-app-id, app_version: #{ready_build.app_version}, build_version: #{ready_build.version}, platform: #{ready_build.platform}") + expect(UI).to receive(:message).with("Waiting for App Store Connect to finish processing the new build (1.0 - 1) for #{ready_build.platform}") + expect(UI).to receive(:success).with("Successfully finished processing the build #{ready_build.app_version} - #{ready_build.version} for #{ready_build.platform}") + found_build = FastlaneCore::BuildWatcher.wait_for_build_processing_to_be_complete(app_id: 'some-app-id', platform: :ios, train_version: '1.0', build_version: '1', return_spaceship_testflight_build: false) + + expect(found_build).to eq(ready_build) + end + end + + describe '1.0.0 with 1.0 alternate' do + let(:processing_build) do + double( + app_version: "1.0.0", + version: "1", + processed?: false, + platform: 'IOS', + processing_state: 'PROCESSING' + ) + end + let(:ready_build) do + double( + app_version: "1.0.0", + version: "1", + processed?: true, + platform: 'IOS', + processing_state: 'VALID' + ) + end + + it 'specific version returns one with alternate returns none' do + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_0).and_return([processing_build]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0).and_return([]) + expect(FastlaneCore::BuildWatcher).to receive(:sleep) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_0).and_return([ready_build]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0).and_return([]) + + expect(UI).to receive(:message).with("Waiting for processing on... app_id: some-app-id, app_version: #{ready_build.app_version}, build_version: #{ready_build.version}, platform: #{ready_build.platform}") + expect(UI).to receive(:message).with("Waiting for App Store Connect to finish processing the new build (1.0.0 - 1) for #{ready_build.platform}") + expect(UI).to receive(:success).with("Successfully finished processing the build #{ready_build.app_version} - #{ready_build.version} for #{ready_build.platform}") + + found_build = FastlaneCore::BuildWatcher.wait_for_build_processing_to_be_complete(app_id: 'some-app-id', platform: :ios, train_version: '1.0.0', build_version: '1', return_spaceship_testflight_build: false) + + expect(found_build).to eq(ready_build) + end + + it 'specific version returns non but alternate returns one' do + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_0).and_return([]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0).and_return([processing_build]) + expect(FastlaneCore::BuildWatcher).to receive(:sleep) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_0).and_return([]) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0).and_return([ready_build]) + + expect(UI).to receive(:message).with("Waiting for processing on... app_id: some-app-id, app_version: #{ready_build.app_version}, build_version: #{ready_build.version}, platform: #{ready_build.platform}") + expect(UI).to receive(:message).with("Waiting for App Store Connect to finish processing the new build (1.0.0 - 1) for #{ready_build.platform}") + expect(UI).to receive(:success).with("Successfully finished processing the build #{ready_build.app_version} - #{ready_build.version} for #{ready_build.platform}") + + expect(UI).to receive(:important).with("App version is 1.0.0 but build was found while querying 1.0") + expect(UI).to receive(:important).with("This shouldn't be an issue as Apple sees 1.0.0 and 1.0 as equal") + expect(UI).to receive(:important).with("See docs for more info - https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html#//apple_ref/doc/uid/20001431-102364") + + found_build = FastlaneCore::BuildWatcher.wait_for_build_processing_to_be_complete(app_id: 'some-app-id', platform: :ios, train_version: '1.0.0', build_version: '1', return_spaceship_testflight_build: false) + + expect(found_build).to eq(ready_build) + end + end + + describe '1.0.1 with no alternate versions' do + let(:processing_build) do + double( + app_version: "1.0.1", + version: "1", + processed?: false, + platform: 'IOS', + processing_state: 'PROCESSING' + ) + end + let(:ready_build) do + double( + app_version: "1.0.1", + version: "1", + processed?: true, + platform: 'IOS', + processing_state: 'VALID' + ) + end + let(:options_1_0_1) do + { app_id: 'some-app-id', version: '1.0.1', build_number: '1', platform: 'IOS' } + end + + it 'specific version returns one with alternate returns none' do + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_1).and_return([processing_build]) + expect(FastlaneCore::BuildWatcher).to receive(:sleep) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_1).and_return([ready_build]) + + expect(UI).to receive(:message).with("Waiting for processing on... app_id: some-app-id, app_version: #{ready_build.app_version}, build_version: #{ready_build.version}, platform: #{ready_build.platform}") + expect(UI).to receive(:message).with("Waiting for App Store Connect to finish processing the new build (1.0.1 - 1) for #{ready_build.platform}") + expect(UI).to receive(:success).with("Successfully finished processing the build #{ready_build.app_version} - #{ready_build.version} for #{ready_build.platform}") + found_build = FastlaneCore::BuildWatcher.wait_for_build_processing_to_be_complete(app_id: 'some-app-id', platform: :ios, train_version: '1.0.1', build_version: '1', return_spaceship_testflight_build: false) + + expect(found_build).to eq(ready_build) + end + + it 'specific version returns non but alternate returns one' do + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_1).and_return([processing_build]) + expect(FastlaneCore::BuildWatcher).to receive(:sleep) + expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_1).and_return([ready_build]) + + expect(UI).to receive(:message).with("Waiting for processing on... app_id: some-app-id, app_version: #{ready_build.app_version}, build_version: #{ready_build.version}, platform: #{ready_build.platform}") + expect(UI).to receive(:message).with("Waiting for App Store Connect to finish processing the new build (1.0.1 - 1) for #{ready_build.platform}") + expect(UI).to receive(:success).with("Successfully finished processing the build #{ready_build.app_version} - #{ready_build.version} for #{ready_build.platform}") + found_build = FastlaneCore::BuildWatcher.wait_for_build_processing_to_be_complete(app_id: 'some-app-id', platform: :ios, train_version: '1.0.1', build_version: '1', return_spaceship_testflight_build: false) + + expect(found_build).to eq(ready_build) + end + end + end end end