fastlane 2.155.1 → 2.157.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +72 -72
- data/deliver/lib/deliver.rb +1 -0
- data/deliver/lib/deliver/app_screenshot_iterator.rb +95 -0
- data/deliver/lib/deliver/detect_values.rb +4 -1
- data/deliver/lib/deliver/languages.rb +7 -0
- data/deliver/lib/deliver/loader.rb +4 -5
- data/deliver/lib/deliver/queue_worker.rb +64 -0
- data/deliver/lib/deliver/runner.rb +7 -5
- data/deliver/lib/deliver/upload_screenshots.rb +143 -128
- data/fastlane/lib/fastlane/actions/app_store_connect_api_key.rb +120 -0
- data/fastlane/lib/fastlane/actions/commit_version_bump.rb +1 -1
- data/fastlane/lib/fastlane/actions/docs/upload_to_play_store.md +2 -0
- data/fastlane/lib/fastlane/actions/docs/upload_to_testflight.md +17 -1
- data/fastlane/lib/fastlane/actions/set_changelog.rb +2 -2
- data/fastlane/lib/fastlane/actions/sonar.rb +5 -0
- data/fastlane/lib/fastlane/actions/spaceship_stats.rb +73 -0
- data/fastlane/lib/fastlane/actions/upload_to_testflight.rb +4 -0
- data/fastlane/lib/fastlane/version.rb +1 -1
- data/fastlane/swift/Deliverfile.swift +1 -1
- data/fastlane/swift/DeliverfileProtocol.swift +1 -1
- data/fastlane/swift/Fastlane.swift +68 -8
- data/fastlane/swift/Gymfile.swift +1 -1
- data/fastlane/swift/GymfileProtocol.swift +1 -1
- data/fastlane/swift/Matchfile.swift +1 -1
- data/fastlane/swift/MatchfileProtocol.swift +1 -1
- data/fastlane/swift/Precheckfile.swift +1 -1
- data/fastlane/swift/PrecheckfileProtocol.swift +1 -1
- data/fastlane/swift/Scanfile.swift +1 -1
- data/fastlane/swift/ScanfileProtocol.swift +1 -1
- data/fastlane/swift/Screengrabfile.swift +1 -1
- data/fastlane/swift/ScreengrabfileProtocol.swift +1 -1
- data/fastlane/swift/Snapshotfile.swift +1 -1
- data/fastlane/swift/SnapshotfileProtocol.swift +1 -1
- data/fastlane_core/lib/fastlane_core/itunes_transporter.rb +71 -42
- data/fastlane_core/lib/fastlane_core/project.rb +1 -0
- data/gym/lib/gym/error_handler.rb +1 -1
- data/gym/lib/gym/generators/build_command_generator.rb +0 -1
- data/pilot/lib/pilot/build_manager.rb +18 -4
- data/pilot/lib/pilot/manager.rb +15 -5
- data/pilot/lib/pilot/options.rb +16 -0
- data/produce/lib/produce/itunes_connect.rb +2 -2
- data/scan/lib/scan/test_command_generator.rb +3 -1
- data/screengrab/lib/screengrab/runner.rb +8 -7
- data/sigh/lib/sigh/runner.rb +9 -5
- data/snapshot/lib/snapshot/test_command_generator_base.rb +3 -1
- data/spaceship/lib/spaceship.rb +4 -0
- data/spaceship/lib/spaceship/client.rb +2 -0
- data/spaceship/lib/spaceship/connect_api.rb +0 -15
- data/spaceship/lib/spaceship/connect_api/api_client.rb +270 -0
- data/spaceship/lib/spaceship/connect_api/client.rb +139 -213
- data/spaceship/lib/spaceship/connect_api/models/profile.rb +3 -2
- data/spaceship/lib/spaceship/connect_api/provisioning/client.rb +8 -17
- data/spaceship/lib/spaceship/connect_api/provisioning/provisioning.rb +75 -64
- data/spaceship/lib/spaceship/connect_api/spaceship.rb +94 -0
- data/spaceship/lib/spaceship/connect_api/testflight/client.rb +8 -17
- data/spaceship/lib/spaceship/connect_api/testflight/testflight.rb +288 -277
- data/spaceship/lib/spaceship/connect_api/token.rb +46 -5
- data/spaceship/lib/spaceship/connect_api/token_refresh_middleware.rb +24 -0
- data/spaceship/lib/spaceship/connect_api/tunes/client.rb +8 -17
- data/spaceship/lib/spaceship/connect_api/tunes/tunes.rb +717 -706
- data/spaceship/lib/spaceship/connect_api/users/client.rb +8 -17
- data/spaceship/lib/spaceship/connect_api/users/users.rb +28 -17
- data/spaceship/lib/spaceship/stats_middleware.rb +65 -0
- metadata +25 -18
- data/sigh/lib/sigh/.runner.rb.swp +0 -0
- data/spaceship/lib/spaceship/connect_api/models/.device.rb.swp +0 -0
| @@ -4,10 +4,17 @@ require 'digest/md5' | |
| 4 4 | 
             
            require_relative 'app_screenshot'
         | 
| 5 5 | 
             
            require_relative 'module'
         | 
| 6 6 | 
             
            require_relative 'loader'
         | 
| 7 | 
            +
            require_relative 'queue_worker'
         | 
| 8 | 
            +
            require_relative 'app_screenshot_iterator'
         | 
| 7 9 |  | 
| 8 10 | 
             
            module Deliver
         | 
| 9 11 | 
             
              # upload screenshots to App Store Connect
         | 
| 10 12 | 
             
              class UploadScreenshots
         | 
| 13 | 
            +
                DeleteScreenshotJob = Struct.new(:app_screenshot, :localization, :app_screenshot_set)
         | 
| 14 | 
            +
                UploadScreenshotJob = Struct.new(:app_screenshot_set, :path)
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                NUMBER_OF_THREADS = Helper.test? ? 1 : [ENV.fetch("DELIVER_NUMBER_OF_THREADS", 10).to_i, 10].min
         | 
| 17 | 
            +
             | 
| 11 18 | 
             
                def upload(options, screenshots)
         | 
| 12 19 | 
             
                  return if options[:skip_screenshots]
         | 
| 13 20 | 
             
                  return if options[:edit_live]
         | 
| @@ -50,57 +57,47 @@ module Deliver | |
| 50 57 | 
             
                    localizations = version.get_app_store_version_localizations
         | 
| 51 58 | 
             
                  end
         | 
| 52 59 |  | 
| 53 | 
            -
                  upload_screenshots( | 
| 60 | 
            +
                  upload_screenshots(localizations, screenshots_per_language)
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                  Helper.show_loading_indicator("Sorting screenshots uploaded...")
         | 
| 63 | 
            +
                  sort_screenshots(localizations)
         | 
| 64 | 
            +
                  Helper.hide_loading_indicator
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  UI.success("Successfully uploaded screenshots to App Store Connect")
         | 
| 54 67 | 
             
                end
         | 
| 55 68 |  | 
| 56 69 | 
             
                def delete_screenshots(localizations, screenshots_per_language, tries: 5)
         | 
| 57 70 | 
             
                  tries -= 1
         | 
| 58 71 |  | 
| 59 | 
            -
                   | 
| 60 | 
            -
             | 
| 61 | 
            -
                     | 
| 62 | 
            -
                     | 
| 63 | 
            -
             | 
| 64 | 
            -
             | 
| 65 | 
            -
             | 
| 66 | 
            -
             | 
| 67 | 
            -
             | 
| 68 | 
            -
             | 
| 69 | 
            -
                    errors = []
         | 
| 70 | 
            -
             | 
| 71 | 
            -
                    screenshot_sets.each do |screenshot_set|
         | 
| 72 | 
            -
                      UI.message("Removing all previously uploaded screenshots for '#{localization.locale}' '#{screenshot_set.screenshot_display_type}'...")
         | 
| 73 | 
            -
                      screenshot_set.app_screenshots.each do |screenshot|
         | 
| 74 | 
            -
                        UI.verbose("Deleting screenshot - #{localization.locale} #{screenshot_set.screenshot_display_type} #{screenshot.id}")
         | 
| 75 | 
            -
                        threads << Thread.new do
         | 
| 76 | 
            -
                          begin
         | 
| 77 | 
            -
                            screenshot.delete!
         | 
| 78 | 
            -
                            UI.verbose("Deleted screenshot - #{localization.locale} #{screenshot_set.screenshot_display_type} #{screenshot.id}")
         | 
| 79 | 
            -
                          rescue => error
         | 
| 80 | 
            -
                            UI.verbose("Failed to delete screenshot - #{localization.locale} #{screenshot_set.screenshot_display_type} #{screenshot.id}")
         | 
| 81 | 
            -
                            errors << error
         | 
| 82 | 
            -
                          end
         | 
| 83 | 
            -
                        end
         | 
| 84 | 
            -
                      end
         | 
| 72 | 
            +
                  worker = QueueWorker.new(NUMBER_OF_THREADS) do |job|
         | 
| 73 | 
            +
                    start_time = Time.now
         | 
| 74 | 
            +
                    target = "#{job.localization.locale} #{job.app_screenshot_set.screenshot_display_type} #{job.app_screenshot.id}"
         | 
| 75 | 
            +
                    begin
         | 
| 76 | 
            +
                      UI.verbose("Deleting '#{target}'")
         | 
| 77 | 
            +
                      job.app_screenshot.delete!
         | 
| 78 | 
            +
                      UI.message("Deleted '#{target}' -  (#{Time.now - start_time} secs)")
         | 
| 79 | 
            +
                    rescue => error
         | 
| 80 | 
            +
                      UI.error("Failed to delete screenshot #{target} - (#{Time.now - start_time} secs)")
         | 
| 81 | 
            +
                      UI.error(error.message)
         | 
| 85 82 | 
             
                    end
         | 
| 83 | 
            +
                  end
         | 
| 86 84 |  | 
| 87 | 
            -
             | 
| 88 | 
            -
             | 
| 89 | 
            -
                     | 
| 90 | 
            -
             | 
| 91 | 
            -
                      threads.each(&:join)
         | 
| 92 | 
            -
                      Helper.hide_loading_indicator unless FastlaneCore::Globals.verbose?
         | 
| 93 | 
            -
                    end
         | 
| 85 | 
            +
                  iterator = AppScreenshotIterator.new(localizations)
         | 
| 86 | 
            +
                  iterator.each_app_screenshot do |localization, app_screenshot_set, app_screenshot|
         | 
| 87 | 
            +
                    # Only delete screenshots if trying to upload
         | 
| 88 | 
            +
                    next unless screenshots_per_language.keys.include?(localization.locale)
         | 
| 94 89 |  | 
| 95 | 
            -
                     | 
| 96 | 
            -
                     | 
| 97 | 
            -
                      UI.error(error.message)
         | 
| 98 | 
            -
                    end
         | 
| 90 | 
            +
                    UI.verbose("Queued delete sceeenshot job for #{localization.locale} #{app_screenshot_set.screenshot_display_type} #{app_screenshot.id}")
         | 
| 91 | 
            +
                    worker.enqueue(DeleteScreenshotJob.new(app_screenshot, localization, app_screenshot_set))
         | 
| 99 92 | 
             
                  end
         | 
| 100 93 |  | 
| 94 | 
            +
                  worker.start
         | 
| 95 | 
            +
             | 
| 101 96 | 
             
                  # Verify all screenshots have been deleted
         | 
| 102 97 | 
             
                  # Sometimes API requests will fail but screenshots will still be deleted
         | 
| 103 | 
            -
                  count =  | 
| 98 | 
            +
                  count = iterator.each_app_screenshot_set.map { |_, app_screenshot_set| app_screenshot_set }
         | 
| 99 | 
            +
                                  .reduce(0) { |sum, app_screenshot_set| sum + app_screenshot_set.app_screenshots.size }
         | 
| 100 | 
            +
             | 
| 104 101 | 
             
                  UI.important("Number of screenshots not deleted: #{count}")
         | 
| 105 102 | 
             
                  if count > 0
         | 
| 106 103 | 
             
                    if tries.zero?
         | 
| @@ -114,113 +111,127 @@ module Deliver | |
| 114 111 | 
             
                  end
         | 
| 115 112 | 
             
                end
         | 
| 116 113 |  | 
| 117 | 
            -
                def  | 
| 118 | 
            -
                   | 
| 119 | 
            -
             | 
| 120 | 
            -
             | 
| 121 | 
            -
             | 
| 122 | 
            -
             | 
| 114 | 
            +
                def upload_screenshots(localizations, screenshots_per_language, tries: 5)
         | 
| 115 | 
            +
                  tries -= 1
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                  # Upload screenshots
         | 
| 118 | 
            +
                  worker = QueueWorker.new(NUMBER_OF_THREADS) do |job|
         | 
| 119 | 
            +
                    begin
         | 
| 120 | 
            +
                      UI.verbose("Uploading '#{job.path}'...")
         | 
| 121 | 
            +
                      start_time = Time.now
         | 
| 122 | 
            +
                      job.app_screenshot_set.upload_screenshot(path: job.path, wait_for_processing: false)
         | 
| 123 | 
            +
                      UI.message("Uploaded '#{job.path}'... (#{Time.now - start_time} secs)")
         | 
| 124 | 
            +
                    rescue => error
         | 
| 125 | 
            +
                      UI.error(error)
         | 
| 123 126 | 
             
                    end
         | 
| 124 127 | 
             
                  end
         | 
| 125 128 |  | 
| 126 | 
            -
                   | 
| 127 | 
            -
             | 
| 129 | 
            +
                  number_of_screenshots = 0
         | 
| 130 | 
            +
                  iterator = AppScreenshotIterator.new(localizations)
         | 
| 131 | 
            +
                  iterator.each_local_screenshot(screenshots_per_language) do |localization, app_screenshot_set, screenshot, index|
         | 
| 132 | 
            +
                    if index >= 10
         | 
| 133 | 
            +
                      UI.error("Too many screenshots found for device '#{screenshot.device_type}' in '#{screenshot.language}', skipping this one (#{screenshot.path})")
         | 
| 134 | 
            +
                      next
         | 
| 135 | 
            +
                    end
         | 
| 128 136 |  | 
| 129 | 
            -
             | 
| 130 | 
            -
             | 
| 131 | 
            -
                  # Default to waiting if submitting for review (since needed for submission)
         | 
| 132 | 
            -
                  # Otherwise use enviroment variable
         | 
| 133 | 
            -
                  if ENV["DELIVER_SKIP_WAIT_FOR_SCREENSHOT_PROCESSING"].nil?
         | 
| 134 | 
            -
                    wait_for_processing = options[:submit_for_review]
         | 
| 135 | 
            -
                    UI.verbose("Setting wait_for_processing from ':submit_for_review' option")
         | 
| 136 | 
            -
                  else
         | 
| 137 | 
            -
                    UI.verbose("Setting wait_for_processing from 'DELIVER_SKIP_WAIT_FOR_SCREENSHOT_PROCESSING' environment variable")
         | 
| 138 | 
            -
                    wait_for_processing = !FastlaneCore::Env.truthy?("DELIVER_SKIP_WAIT_FOR_SCREENSHOT_PROCESSING")
         | 
| 139 | 
            -
                  end
         | 
| 137 | 
            +
                    checksum = UploadScreenshots.calculate_checksum(screenshot.path)
         | 
| 138 | 
            +
                    duplicate = (app_screenshot_set.app_screenshots || []).any? { |s| s.source_file_checksum == checksum }
         | 
| 140 139 |  | 
| 141 | 
            -
             | 
| 142 | 
            -
                     | 
| 143 | 
            -
             | 
| 144 | 
            -
             | 
| 145 | 
            -
             | 
| 146 | 
            -
                     | 
| 140 | 
            +
                    # Enqueue uploading job if it's not duplicated otherwise screenshot will be skipped
         | 
| 141 | 
            +
                    if duplicate
         | 
| 142 | 
            +
                      UI.message("Previous uploaded. Skipping '#{screenshot.path}'...")
         | 
| 143 | 
            +
                    else
         | 
| 144 | 
            +
                      worker.enqueue(UploadScreenshotJob.new(app_screenshot_set, screenshot.path))
         | 
| 145 | 
            +
                    end
         | 
| 146 | 
            +
             | 
| 147 | 
            +
                    number_of_screenshots += 1
         | 
| 147 148 | 
             
                  end
         | 
| 148 149 |  | 
| 149 | 
            -
                   | 
| 150 | 
            -
                  indized = {} # per language and device type
         | 
| 150 | 
            +
                  worker.start
         | 
| 151 151 |  | 
| 152 | 
            -
                   | 
| 153 | 
            -
                    # Find localization to upload screenshots to
         | 
| 154 | 
            -
                    localization = localizations.find do |l|
         | 
| 155 | 
            -
                      l.locale == language
         | 
| 156 | 
            -
                    end
         | 
| 152 | 
            +
                  UI.verbose('Uploading jobs are completed')
         | 
| 157 153 |  | 
| 158 | 
            -
             | 
| 159 | 
            -
             | 
| 160 | 
            -
             | 
| 161 | 
            -
             | 
| 154 | 
            +
                  Helper.show_loading_indicator("Waiting for all the screenshots processed...")
         | 
| 155 | 
            +
                  states = wait_for_complete(iterator)
         | 
| 156 | 
            +
                  Helper.hide_loading_indicator
         | 
| 157 | 
            +
                  retry_upload_screenshots_if_needed(iterator, states, number_of_screenshots, tries, localizations, screenshots_per_language)
         | 
| 162 158 |  | 
| 163 | 
            -
             | 
| 159 | 
            +
                  UI.message("Successfully uploaded all screenshots")
         | 
| 160 | 
            +
                end
         | 
| 164 161 |  | 
| 165 | 
            -
             | 
| 166 | 
            -
             | 
| 167 | 
            -
             | 
| 168 | 
            -
                     | 
| 169 | 
            -
                       | 
| 162 | 
            +
                # Verify all screenshots have been processed
         | 
| 163 | 
            +
                def wait_for_complete(iterator)
         | 
| 164 | 
            +
                  loop do
         | 
| 165 | 
            +
                    states = iterator.each_app_screenshot.map { |_, _, app_screenshot| app_screenshot }.each_with_object({}) do |app_screenshot, hash|
         | 
| 166 | 
            +
                      state = app_screenshot.asset_delivery_state['state']
         | 
| 167 | 
            +
                      hash[state] ||= 0
         | 
| 168 | 
            +
                      hash[state] += 1
         | 
| 169 | 
            +
                    end
         | 
| 170 170 |  | 
| 171 | 
            -
             | 
| 172 | 
            -
             | 
| 173 | 
            -
                        count: app_screenshot_set.app_screenshots.size,
         | 
| 174 | 
            -
                        checksums: []
         | 
| 175 | 
            -
                      }
         | 
| 171 | 
            +
                    is_processing = states.fetch('UPLOAD_COMPLETE', 0) > 0
         | 
| 172 | 
            +
                    return states unless is_processing
         | 
| 176 173 |  | 
| 177 | 
            -
             | 
| 178 | 
            -
             | 
| 179 | 
            -
             | 
| 174 | 
            +
                    UI.verbose("There are still incomplete screenshots - #{states}")
         | 
| 175 | 
            +
                    sleep(5)
         | 
| 176 | 
            +
                  end
         | 
| 177 | 
            +
                end
         | 
| 180 178 |  | 
| 181 | 
            -
             | 
| 182 | 
            -
             | 
| 183 | 
            -
             | 
| 184 | 
            -
             | 
| 179 | 
            +
                # Verify all screenshots states on App Store Connect are okay
         | 
| 180 | 
            +
                def retry_upload_screenshots_if_needed(iterator, states, number_of_screenshots, tries, localizations, screenshots_per_language)
         | 
| 181 | 
            +
                  is_failure = states.fetch("FAILED", 0) > 0
         | 
| 182 | 
            +
                  is_missing_screenshot = !screenshots_per_language.empty? && !verify_local_screenshots_are_uploaded(iterator, screenshots_per_language)
         | 
| 183 | 
            +
                  return unless is_failure || is_missing_screenshot
         | 
| 185 184 |  | 
| 186 | 
            -
             | 
| 187 | 
            -
             | 
| 188 | 
            -
             | 
| 189 | 
            -
             | 
| 185 | 
            +
                  if tries.zero?
         | 
| 186 | 
            +
                    iterator.each_app_screenshot.select { |_, _, app_screenshot| app_screenshot.error? }.each do |localization, _, app_screenshot|
         | 
| 187 | 
            +
                      UI.error("#{app_screenshot.file_name} for #{localization.locale} has error(s) - #{app_screenshot.error_messages.join(', ')}")
         | 
| 188 | 
            +
                    end
         | 
| 189 | 
            +
                    incomplete_screenshot_count = states.reject { |k, v| k == 'COMPLETE' }.reduce(0) { |sum, (k, v)| sum + v }
         | 
| 190 | 
            +
                    UI.user_error!("Failed verification of all screenshots uploaded... #{incomplete_screenshot_count} incomplete screenshot(s) still exist")
         | 
| 191 | 
            +
                  else
         | 
| 192 | 
            +
                    UI.error("Failed to upload all screenshots... Tries remaining: #{tries}")
         | 
| 193 | 
            +
                    # Delete bad entries before retry
         | 
| 194 | 
            +
                    iterator.each_app_screenshot do |_, _, app_screenshot|
         | 
| 195 | 
            +
                      app_screenshot.delete! unless app_screenshot.complete?
         | 
| 196 | 
            +
                    end
         | 
| 197 | 
            +
                    upload_screenshots(localizations, screenshots_per_language, tries: tries)
         | 
| 198 | 
            +
                  end
         | 
| 199 | 
            +
                end
         | 
| 190 200 |  | 
| 191 | 
            -
             | 
| 192 | 
            -
             | 
| 193 | 
            -
             | 
| 194 | 
            -
             | 
| 195 | 
            -
                        app_screenshot_sets_map[display_type] = set
         | 
| 201 | 
            +
                # Return `true` if all the local screenshots are uploaded to App Store Connect
         | 
| 202 | 
            +
                def verify_local_screenshots_are_uploaded(iterator, screenshots_per_language)
         | 
| 203 | 
            +
                  # Check if local screenshots' checksum exist on App Store Connect
         | 
| 204 | 
            +
                  checksum_to_app_screenshot = iterator.each_app_screenshot.map { |_, _, app_screenshot| [app_screenshot.source_file_checksum, app_screenshot] }.to_h
         | 
| 196 205 |  | 
| 197 | 
            -
             | 
| 198 | 
            -
             | 
| 199 | 
            -
             | 
| 200 | 
            -
             | 
| 201 | 
            -
                      end
         | 
| 206 | 
            +
                  missing_local_screenshots = iterator.each_local_screenshot(screenshots_per_language).select do |_, _, local_screenshot, index|
         | 
| 207 | 
            +
                    checksum = UploadScreenshots.calculate_checksum(local_screenshot.path)
         | 
| 208 | 
            +
                    checksum_to_app_screenshot[checksum].nil? && index < 10 # if index is more than 10, it's skipped
         | 
| 209 | 
            +
                  end
         | 
| 202 210 |  | 
| 203 | 
            -
             | 
| 211 | 
            +
                  missing_local_screenshots.each do |_, _, screenshot, _|
         | 
| 212 | 
            +
                    UI.error("#{screenshot.path} is missing on App Store Connect.")
         | 
| 213 | 
            +
                  end
         | 
| 204 214 |  | 
| 205 | 
            -
             | 
| 206 | 
            -
             | 
| 207 | 
            -
                        next
         | 
| 208 | 
            -
                      end
         | 
| 215 | 
            +
                  missing_local_screenshots.empty?
         | 
| 216 | 
            +
                end
         | 
| 209 217 |  | 
| 210 | 
            -
             | 
| 211 | 
            -
             | 
| 212 | 
            -
                      duplicate = indized[localization.locale][set.screenshot_display_type][:checksums].include?(checksum)
         | 
| 218 | 
            +
                def sort_screenshots(localizations)
         | 
| 219 | 
            +
                  iterator = AppScreenshotIterator.new(localizations)
         | 
| 213 220 |  | 
| 214 | 
            -
             | 
| 215 | 
            -
             | 
| 216 | 
            -
             | 
| 217 | 
            -
             | 
| 218 | 
            -
             | 
| 219 | 
            -
             | 
| 220 | 
            -
                      end
         | 
| 221 | 
            +
                  # Re-order screenshots within app_screenshot_set
         | 
| 222 | 
            +
                  worker = QueueWorker.new(NUMBER_OF_THREADS) do |app_screenshot_set|
         | 
| 223 | 
            +
                    original_ids = app_screenshot_set.app_screenshots.map(&:id)
         | 
| 224 | 
            +
                    sorted_ids = app_screenshot_set.app_screenshots.sort_by(&:file_name).map(&:id)
         | 
| 225 | 
            +
                    if original_ids != sorted_ids
         | 
| 226 | 
            +
                      app_screenshot_set.reorder_screenshots(app_screenshot_ids: sorted_ids)
         | 
| 221 227 | 
             
                    end
         | 
| 222 228 | 
             
                  end
         | 
| 223 | 
            -
             | 
| 229 | 
            +
             | 
| 230 | 
            +
                  iterator.each_app_screenshot_set do |_, app_screenshot_set|
         | 
| 231 | 
            +
                    worker.enqueue(app_screenshot_set)
         | 
| 232 | 
            +
                  end
         | 
| 233 | 
            +
             | 
| 234 | 
            +
                  worker.start
         | 
| 224 235 | 
             
                end
         | 
| 225 236 |  | 
| 226 237 | 
             
                def collect_screenshots(options)
         | 
| @@ -293,11 +304,15 @@ module Deliver | |
| 293 304 |  | 
| 294 305 | 
             
                # helper method so Spaceship::Tunes.client.available_languages is easier to test
         | 
| 295 306 | 
             
                def self.available_languages
         | 
| 296 | 
            -
                   | 
| 297 | 
            -
             | 
| 298 | 
            -
                   | 
| 299 | 
            -
             | 
| 300 | 
            -
             | 
| 307 | 
            +
                  # 2020-08-24 - Available locales are not available as an endpoint in App Store Connect
         | 
| 308 | 
            +
                  # Update with Spaceship::Tunes.client.available_languages.sort (as long as endpoint is avilable)
         | 
| 309 | 
            +
                  Deliver::Languages::ALL_LANGUAGES
         | 
| 310 | 
            +
                end
         | 
| 311 | 
            +
             | 
| 312 | 
            +
                # helper method to mock this step in tests
         | 
| 313 | 
            +
                def self.calculate_checksum(path)
         | 
| 314 | 
            +
                  bytes = File.binread(path)
         | 
| 315 | 
            +
                  Digest::MD5.hexdigest(bytes)
         | 
| 301 316 | 
             
                end
         | 
| 302 317 | 
             
              end
         | 
| 303 318 | 
             
            end
         | 
| @@ -0,0 +1,120 @@ | |
| 1 | 
            +
            module Fastlane
         | 
| 2 | 
            +
              module Actions
         | 
| 3 | 
            +
                module SharedValues
         | 
| 4 | 
            +
                  APP_STORE_CONNECT_API_KEY = :APP_STORE_CONNECT_API_KEY
         | 
| 5 | 
            +
                end
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                class AppStoreConnectApiKeyAction < Action
         | 
| 8 | 
            +
                  def self.run(options)
         | 
| 9 | 
            +
                    key_id = options[:key_id]
         | 
| 10 | 
            +
                    issuer_id = options[:issuer_id]
         | 
| 11 | 
            +
                    key_content = options[:key_content]
         | 
| 12 | 
            +
                    key_filepath = options[:key_filepath]
         | 
| 13 | 
            +
                    duration = options[:duration]
         | 
| 14 | 
            +
                    in_house = options[:in_house]
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                    if key_content.nil? && key_filepath.nil?
         | 
| 17 | 
            +
                      UI.user_error!(":key_content or :key_filepath is required")
         | 
| 18 | 
            +
                    end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    # This hash matches the named arguments on
         | 
| 21 | 
            +
                    # the Spaceship::ConnectAPI::Token.create method
         | 
| 22 | 
            +
                    key = {
         | 
| 23 | 
            +
                      key_id: key_id,
         | 
| 24 | 
            +
                      issuer_id: issuer_id,
         | 
| 25 | 
            +
                      key: key_content || File.binread(key_filepath),
         | 
| 26 | 
            +
                      duration: duration,
         | 
| 27 | 
            +
                      in_house: in_house
         | 
| 28 | 
            +
                    }
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                    Actions.lane_context[SharedValues::APP_STORE_CONNECT_API_KEY] = key
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                    return key
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  def self.description
         | 
| 36 | 
            +
                    "Load the App Store Connect API token to use in other fastlane tools and actions"
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  def self.available_options
         | 
| 40 | 
            +
                    [
         | 
| 41 | 
            +
                      FastlaneCore::ConfigItem.new(key: :key_id,
         | 
| 42 | 
            +
                                                   env_name: "APP_STORE_CONNECT_API_KEY_KEY_ID",
         | 
| 43 | 
            +
                                                   description: "The key ID"),
         | 
| 44 | 
            +
                      FastlaneCore::ConfigItem.new(key: :issuer_id,
         | 
| 45 | 
            +
                                                   env_name: "APP_STORE_CONNECT_API_KEY_ISSUER_ID",
         | 
| 46 | 
            +
                                                   description: "The issuer ID"),
         | 
| 47 | 
            +
                      FastlaneCore::ConfigItem.new(key: :key_filepath,
         | 
| 48 | 
            +
                                                   env_name: "APP_STORE_CONNECT_API_KEY_KEY_FILEPATH",
         | 
| 49 | 
            +
                                                   description: "The path to the key p8 file",
         | 
| 50 | 
            +
                                                   optional: true,
         | 
| 51 | 
            +
                                                   conflicting_options: [:key_content],
         | 
| 52 | 
            +
                                                   verify_block: proc do |value|
         | 
| 53 | 
            +
                                                     UI.user_error!("Couldn't find key p8 file at path '#{value}'") unless File.exist?(value)
         | 
| 54 | 
            +
                                                   end),
         | 
| 55 | 
            +
                      FastlaneCore::ConfigItem.new(key: :key_content,
         | 
| 56 | 
            +
                                                   env_name: "APP_STORE_CONNECT_API_KEY_KEY",
         | 
| 57 | 
            +
                                                   description: "The content of the key p8 file",
         | 
| 58 | 
            +
                                                   optional: true,
         | 
| 59 | 
            +
                                                   conflicting_options: [:filepath]),
         | 
| 60 | 
            +
                      FastlaneCore::ConfigItem.new(key: :duration,
         | 
| 61 | 
            +
                                                   env_name: "APP_STORE_CONNECT_API_KEY_DURATION",
         | 
| 62 | 
            +
                                                   description: "The token session duration",
         | 
| 63 | 
            +
                                                   optional: true,
         | 
| 64 | 
            +
                                                   type: Integer),
         | 
| 65 | 
            +
                      FastlaneCore::ConfigItem.new(key: :in_house,
         | 
| 66 | 
            +
                                                   env_name: "APP_STORE_CONNECT_API_KEY_IN_HOUSE",
         | 
| 67 | 
            +
                                                   description: "Is App Store or Enterprise (in house) team? App Store Connect API cannot not determine this on its own (yet)",
         | 
| 68 | 
            +
                                                   optional: true,
         | 
| 69 | 
            +
                                                   type: Boolean)
         | 
| 70 | 
            +
                    ]
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                  def self.output
         | 
| 74 | 
            +
                    [
         | 
| 75 | 
            +
                      ['APP_STORE_CONNECT_API_KEY', 'The App Store Connect API key information used for authorization requests. This hash can be passed directly into the :api_key options on other tools or into Spaceship::ConnectAPI::Token.create method']
         | 
| 76 | 
            +
                    ]
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  def self.author
         | 
| 80 | 
            +
                    ["joshdholtz"]
         | 
| 81 | 
            +
                  end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                  def self.is_supported?(platform)
         | 
| 84 | 
            +
                    true
         | 
| 85 | 
            +
                  end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                  def self.details
         | 
| 88 | 
            +
                    [
         | 
| 89 | 
            +
                      "Load the App Store Connect API token to use in other fastlane tools and actions"
         | 
| 90 | 
            +
                    ].join("\n")
         | 
| 91 | 
            +
                  end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                  def self.example_code
         | 
| 94 | 
            +
                    [
         | 
| 95 | 
            +
                      'app_store_connect_api_key(
         | 
| 96 | 
            +
                        key_id: "D83848D23",
         | 
| 97 | 
            +
                        issuer_id: "227b0bbf-ada8-458c-9d62-3d8022b7d07f",
         | 
| 98 | 
            +
                        key_filepath: "D83848D23.p8"
         | 
| 99 | 
            +
                      )',
         | 
| 100 | 
            +
                      'app_store_connect_api_key(
         | 
| 101 | 
            +
                        key_id: "D83848D23",
         | 
| 102 | 
            +
                        issuer_id: "227b0bbf-ada8-458c-9d62-3d8022b7d07f",
         | 
| 103 | 
            +
                        key_filepath: "D83848D23.p8",
         | 
| 104 | 
            +
                        duration: 200,
         | 
| 105 | 
            +
                        in_house: true
         | 
| 106 | 
            +
                      )',
         | 
| 107 | 
            +
                      'app_store_connect_api_key(
         | 
| 108 | 
            +
                        key_id: "D83848D23",
         | 
| 109 | 
            +
                        issuer_id: "227b0bbf-ada8-458c-9d62-3d8022b7d07f",
         | 
| 110 | 
            +
                        key_content: "-----BEGIN EC PRIVATE KEY-----\nfewfawefawfe\n-----END EC PRIVATE KEY-----"
         | 
| 111 | 
            +
                      )'
         | 
| 112 | 
            +
                    ]
         | 
| 113 | 
            +
                  end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                  def self.category
         | 
| 116 | 
            +
                    :app_store_connect
         | 
| 117 | 
            +
                  end
         | 
| 118 | 
            +
                end
         | 
| 119 | 
            +
              end
         | 
| 120 | 
            +
            end
         |