Skip to content

Commit

Permalink
[deliver] increase chances of success when creating a new app version…
Browse files Browse the repository at this point in the history
… even when Apple servers are degraded (#21742)

* Add exponential backoff to retry mechanism that checks for app version existence.

* Add option for users to increase the number of max retries if needed.

* Fix linter issue complaining about long line.

* Set max wait time to 5 minutes.

* Adjust default number of retries to a more sensible number.

* [deliver] Initialize UploadMetadata with options (#21861)

* [deliver] Initialize UploadMetadata with options

* Fix linting

---------

Co-authored-by: Daniel Jankowski <daniell.jankowskii@gmail.com>
  • Loading branch information
rogerluan and mollyIV committed Feb 9, 2024
1 parent 26b695f commit 4c8f207
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 133 deletions.
2 changes: 1 addition & 1 deletion deliver/lib/deliver/generate_summary.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module Deliver
class GenerateSummary
def run(options)
screenshots = UploadScreenshots.new.collect_screenshots(options)
UploadMetadata.new.load_from_filesystem(options)
UploadMetadata.new(options).load_from_filesystem
HtmlGenerator.new.render(options, screenshots, '.')
end
end
Expand Down
9 changes: 9 additions & 0 deletions deliver/lib/deliver/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,15 @@ def self.available_options
description: "Rejects the previously submitted build if it's in a state where it's possible",
type: Boolean,
default_value: false),
FastlaneCore::ConfigItem.new(key: :version_check_wait_retry_limit,
env_name: "DELIVER_VERSION_CHECK_WAIT_RETRY_LIMIT",
description: "After submitting a new version, App Store Connect takes some time to recognize the new version and we must wait until it's available before attempting to upload metadata for it. There is a mechanism that will check if it's available and retry with an exponential backoff if it's not available yet. " \
"This option specifies how many times we should retry before giving up. Setting this to a value below 5 is not recommended and will likely cause failures. Increase this parameter when Apple servers seem to be degraded or slow",
type: Integer,
default_value: 7,
verify_block: proc do |value|
UI.user_error!("'#{value}' needs to be greater than 0") if value <= 0
end),

# release
FastlaneCore::ConfigItem.new(key: :automatic_release,
Expand Down
8 changes: 4 additions & 4 deletions deliver/lib/deliver/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,21 +135,21 @@ def verify_version

# Upload all metadata, screenshots, pricing information, etc. to App Store Connect
def upload_metadata
upload_metadata = UploadMetadata.new
upload_metadata = UploadMetadata.new(options)
upload_screenshots = UploadScreenshots.new

# First, collect all the things for the HTML Report
screenshots = upload_screenshots.collect_screenshots(options)
upload_metadata.load_from_filesystem(options)
upload_metadata.load_from_filesystem

# Assign "default" values to all languages
upload_metadata.assign_defaults(options)
upload_metadata.assign_defaults

# Validate
validate_html(screenshots)

# Commit
upload_metadata.upload(options)
upload_metadata.upload

if options[:sync_screenshots]
sync_screenshots = SyncScreenshots.new(app: Deliver.cache[:app], platform: Spaceship::ConnectAPI::Platform.map(options[:platform]))
Expand Down
71 changes: 43 additions & 28 deletions deliver/lib/deliver/upload_metadata.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,19 +78,25 @@ class UploadMetadata

require_relative 'loader'

attr_accessor :options

def initialize(options)
self.options = options
end

# Make sure to call `load_from_filesystem` before calling upload
def upload(options)
def upload
return if options[:skip_metadata]

app = Deliver.cache[:app]

platform = Spaceship::ConnectAPI::Platform.map(options[:platform])

enabled_languages = detect_languages(options)
enabled_languages = detect_languages

app_store_version_localizations = verify_available_version_languages!(options, app, enabled_languages) unless options[:edit_live]
app_store_version_localizations = verify_available_version_languages!(app, enabled_languages) unless options[:edit_live]
app_info = fetch_edit_app_info(app)
app_info_localizations = verify_available_info_languages!(options, app, app_info, enabled_languages) unless options[:edit_live] || !updating_localized_app_info?(options, app, app_info)
app_info_localizations = verify_available_info_languages!(app, app_info, enabled_languages) unless options[:edit_live] || !updating_localized_app_info?(app, app_info)

if options[:edit_live]
# not all values are editable when using live_version
Expand Down Expand Up @@ -342,9 +348,9 @@ def upload(options)
end
end

set_review_information(version, options)
set_review_attachment_file(version, options)
set_app_rating(app_info, options)
review_information(version)
review_attachment_file(version)
app_rating(app_info)
end

# rubocop:enable Metrics/PerceivedComplexity
Expand All @@ -360,12 +366,12 @@ def convert_ms_to_iso8601(time_in_ms)
end

# If the user is using the 'default' language, then assign values where they are needed
def assign_defaults(options)
def assign_defaults
# Normalizes languages keys from symbols to strings
normalize_language_keys(options)
normalize_language_keys

# Build a complete list of the required languages
enabled_languages = detect_languages(options)
enabled_languages = detect_languages

# Get all languages used in existing settings
(LOCALISED_VERSION_VALUES.keys + LOCALISED_APP_VALUES.keys).each do |key|
Expand Down Expand Up @@ -402,7 +408,7 @@ def assign_defaults(options)
end
end

def detect_languages(options)
def detect_languages
# Build a complete list of the required languages
enabled_languages = options[:languages] || []

Expand All @@ -427,40 +433,49 @@ def detect_languages(options)
.uniq
end

def fetch_edit_app_store_version(app, platform, wait_time: 10)
retry_if_nil("Cannot find edit app store version", wait_time: wait_time) do
def fetch_edit_app_store_version(app, platform)
retry_if_nil("Cannot find edit app store version") do
app.get_edit_app_store_version(platform: platform)
end
end

def fetch_edit_app_info(app, wait_time: 10)
retry_if_nil("Cannot find edit app info", wait_time: wait_time) do
def fetch_edit_app_info(app)
retry_if_nil("Cannot find edit app info") do
app.fetch_edit_app_info
end
end

def fetch_live_app_info(app, wait_time: 10)
retry_if_nil("Cannot find live app info", wait_time: wait_time) do
def fetch_live_app_info(app)
retry_if_nil("Cannot find live app info") do
app.fetch_live_app_info
end
end

def retry_if_nil(message, tries: 5, wait_time: 10)
# Retries a block of code if the return value is nil, with an exponential backoff.
def retry_if_nil(message)
tries = options[:version_check_wait_retry_limit]
wait_time = 10
loop do
tries -= 1

value = yield
return value if value

UI.message("#{message}... Retrying after #{wait_time} seconds (remaining: #{tries})")
sleep(wait_time)
# Calculate sleep time to be the lesser of the exponential backoff or 5 minutes.
# This prevents problems with CI's console output timeouts (of usually 10 minutes), and also
# speeds up the retry time for the user, as waiting longer than 5 minutes is a too long wait for a retry.
sleep_time = [wait_time * 2, 5 * 60].min
UI.message("#{message}... Retrying after #{sleep_time} seconds (remaining: #{tries})")
Kernel.sleep(sleep_time)

return nil if tries.zero?

wait_time *= 2 # Double the wait time for the next iteration
end
end

# Checking if the metadata to update includes localised App Info
def updating_localized_app_info?(options, app, app_info)
def updating_localized_app_info?(app, app_info)
app_info ||= fetch_live_app_info(app)
unless app_info
UI.important("Can't find edit or live App info. Skipping upload.")
Expand Down Expand Up @@ -499,7 +514,7 @@ def updating_localized_app_info?(options, app, app_info)
end

# Finding languages to enable
def verify_available_info_languages!(options, app, app_info, languages)
def verify_available_info_languages!(app, app_info, languages)
unless app_info
UI.user_error!("Cannot update languages - could not find an editable 'App Info'. Verify that your app is in one of the editable states in App Store Connect")
return
Expand Down Expand Up @@ -531,7 +546,7 @@ def verify_available_info_languages!(options, app, app_info, languages)
end

# Finding languages to enable
def verify_available_version_languages!(options, app, languages)
def verify_available_version_languages!(app, languages)
platform = Spaceship::ConnectAPI::Platform.map(options[:platform])
version = fetch_edit_app_store_version(app, platform)

Expand Down Expand Up @@ -566,7 +581,7 @@ def verify_available_version_languages!(options, app, languages)
end

# Loads the metadata files and stores them into the options object
def load_from_filesystem(options)
def load_from_filesystem
return if options[:skip_metadata]

# Load localised data
Expand Down Expand Up @@ -623,7 +638,7 @@ def load_from_filesystem(options)
private

# Normalizes languages keys from symbols to strings
def normalize_language_keys(options)
def normalize_language_keys
(LOCALISED_VERSION_VALUES.keys + LOCALISED_APP_VALUES.keys).each do |key|
current = options[key]
next unless current && current.kind_of?(Hash)
Expand All @@ -636,7 +651,7 @@ def normalize_language_keys(options)
options
end

def set_review_information(version, options)
def review_information(version)
info = options[:app_review_information]
return if info.nil? || info.empty?

Expand Down Expand Up @@ -669,7 +684,7 @@ def set_review_information(version, options)
end
end

def set_review_attachment_file(version, options)
def review_attachment_file(version)
app_store_review_detail = version.fetch_app_store_review_detail
app_store_review_attachments = app_store_review_detail.app_store_review_attachments || []

Expand All @@ -687,7 +702,7 @@ def set_review_attachment_file(version, options)
end
end

def set_app_rating(app_info, options)
def app_rating(app_info)
return unless options[:app_rating_config_path]

require 'json'
Expand Down

0 comments on commit 4c8f207

Please sign in to comment.