fastlane-plugin-wpmreleasetoolkit 15.0.0 → 15.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d947683766d2a3fddc5ab9ccf7182bc628ca63d839e94ab6633cead85d91fc76
4
- data.tar.gz: 3278ec05690d03d229b938b711e8e4ecf59fe2945008672ebc81b7a5edbfe555
3
+ metadata.gz: f47aea4602fae982122ed3c370c7cbc3330ebe242781ff4abffb4498016f384c
4
+ data.tar.gz: e4526ac1fcbb27ece5e15fc8a11ea60d453c2bbd1ab34a8f1c88040f590e31f8
5
5
  SHA512:
6
- metadata.gz: 5512d2f282436ade2e641cefb4f5e4b3bb5db9a6c857a1d20841cebf45339c4e4e485e994c6094eb3f1a5eb78a284ef5826edaf0712d51c78b012def1f0bed3a
7
- data.tar.gz: fa586cd530a3bb5463e3038ae9e057cab05d8166ab9d69aedbc823824f2770e14efc8afb70674fd66a4e6bc5790e6dfe753a9469eecdd6d1a809176bd9c7a4d4
6
+ metadata.gz: 297833bfc56d46af40b9771485fe42cc5b2e54c910ddf92ea2701e50088bb4015176ed988544e593128569e3f410093cb0421976b32167e4c9755c423fc44c0d
7
+ data.tar.gz: 39d1cd8e4a1cc8fde878d10538cc5a49cb6b2fa58ad2f2a80fb3d1c2ea4fa830c8e4803f7052fdbd243619b4dcc60a150f4b522504f218327dd4145f3b15dee5
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../../helper/config_item_helper'
4
+
3
5
  # This action is the new version of android_update_metadata (AndroidUpdateMetadataAction) and should now be used instead of that one
4
6
 
5
7
  module Fastlane
@@ -19,7 +21,8 @@ module Fastlane
19
21
  res_dir: res_dir,
20
22
  glotpress_project_url: params[:glotpress_url],
21
23
  glotpress_filters: params[:status_filter].map { |s| { status: s } },
22
- locales_map: params[:locales]
24
+ locales_map: params[:locales],
25
+ fail_on_error: params[:fail_on_error]
23
26
  )
24
27
 
25
28
  # Update submodules then lint translations
@@ -99,6 +102,7 @@ module Fastlane
99
102
  type: Boolean,
100
103
  default_value: false
101
104
  ),
105
+ Fastlane::Helper::ConfigItemHelper.opt_in_fail_on_error_config_item,
102
106
  ]
103
107
  end
104
108
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'fastlane/action'
4
+ require_relative '../../helper/config_item_helper'
4
5
  require_relative '../../helper/github_helper'
5
6
 
6
7
  module Fastlane
@@ -104,15 +105,7 @@ module Fastlane
104
105
  'See https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes#configuration-options',
105
106
  optional: true,
106
107
  type: String),
107
- FastlaneCore::ConfigItem.new(key: :fail_on_error,
108
- description: 'Whether to fail the lane if the changelog cannot be computed. ' \
109
- 'When `false` (the default), the error message is returned as the changelog itself, ' \
110
- 'so that it ends up visible in the GitHub Release body. ' \
111
- 'Set this to `true` if the caller publishes the release only after this action succeeds, ' \
112
- 'and would rather stop than publish a release whose notes are an error message',
113
- optional: true,
114
- default_value: false,
115
- type: Boolean),
108
+ Fastlane::Helper::ConfigItemHelper.opt_in_fail_on_error_config_item,
116
109
  Fastlane::Helper::GithubHelper.github_token_config_item,
117
110
  ]
118
111
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'fastlane/action'
4
4
  require_relative '../../helper/metadata_download_helper'
5
+ require_relative '../../helper/config_item_helper'
5
6
 
6
7
  module Fastlane
7
8
  module Actions
@@ -13,12 +14,13 @@ module Fastlane
13
14
  UI.message "Source locale: #{params[:source_locale].nil? ? '-' : params[:source_locale]}"
14
15
  UI.message "Path: #{params[:download_path]}"
15
16
  UI.message "Auto-retry: #{params[:auto_retry]}"
17
+ UI.message "Fail on error: #{params[:fail_on_error]}"
16
18
 
17
19
  # Check download path
18
20
  FileUtils.mkdir_p(params[:download_path])
19
21
 
20
22
  # Download
21
- downloader = Fastlane::Helper::MetadataDownloader.new(params[:download_path], params[:target_files], params[:auto_retry])
23
+ downloader = Fastlane::Helper::MetadataDownloader.new(params[:download_path], params[:target_files], params[:auto_retry], fail_on_error: params[:fail_on_error])
22
24
 
23
25
  params[:locales].each do |loc|
24
26
  if loc.is_a?(Array)
@@ -77,6 +79,7 @@ module Fastlane
77
79
  type: FastlaneCore::Boolean,
78
80
  optional: true,
79
81
  default_value: true),
82
+ Fastlane::Helper::ConfigItemHelper.opt_in_fail_on_error_config_item,
80
83
  ]
81
84
  end
82
85
 
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'tempfile'
4
+ require_relative '../../helper/config_item_helper'
5
+
3
6
  module Fastlane
4
7
  module Actions
5
8
  class IosDownloadStringsFilesFromGlotpressAction < Action
@@ -17,33 +20,91 @@ module Fastlane
17
20
  destination = File.join(lproj_dir, "#{params[:table_basename]}.strings")
18
21
  FileUtils.mkdir_p(lproj_dir)
19
22
 
20
- Fastlane::Helper::Ios::L10nHelper.download_glotpress_export_file(
23
+ download_and_replace_strings_file(
21
24
  project_url: params[:project_url],
22
25
  locale: glotpress_locale,
23
26
  filters: params[:filters],
24
- destination: destination
27
+ destination: destination,
28
+ table_basename: params[:table_basename],
29
+ skip_file_validation: params[:skip_file_validation],
30
+ fail_on_error: params[:fail_on_error]
25
31
  )
26
- # Do a quick check of the downloaded `.strings` file to ensure it looks valid
27
- validate_strings_file(destination) unless params[:skip_file_validation]
28
32
  end
29
33
  end
30
34
 
31
- # Validate that a `.strings` file downloaded from GlotPress seems valid and does not contain empty translations
32
- def self.validate_strings_file(destination)
33
- return unless File.exist?(destination) # If the file failed to download, don't try to validate an non-existing file. We'd already have a separate error for the download failure anyway.
35
+ def self.download_and_replace_strings_file(project_url:, locale:, filters:, destination:, table_basename:, skip_file_validation:, fail_on_error:)
36
+ if File.exist?(destination) && !File.file?(destination)
37
+ report_error("The destination `#{destination}` exists but is not a regular file", fail_on_error: fail_on_error)
38
+ return false
39
+ end
34
40
 
35
- translations = Fastlane::Helper::Ios::L10nHelper.read_strings_file_as_hash(path: destination)
36
- empty_keys = translations.select { |_, value| value.nil? || value.empty? }.keys.sort
37
- unless empty_keys.empty?
38
- UI.error(
39
- "Found empty translations in `#{destination}` for the following keys: #{empty_keys.inspect}.\n" \
40
- + "This is likely a GlotPress bug, and will lead to copies replaced by empty text in the UI.\n" \
41
- + 'Please report this to the GlotPress team, and fix the file locally before continuing.'
41
+ destination_mode = File.exist?(destination) ? File.stat(destination).mode & 0o7777 : 0o644
42
+ Tempfile.create([table_basename, '.strings'], File.dirname(destination)) do |temporary_file|
43
+ downloaded = Fastlane::Helper::Ios::L10nHelper.download_glotpress_export_file(
44
+ project_url: project_url,
45
+ locale: locale,
46
+ filters: filters,
47
+ destination: temporary_file,
48
+ fail_on_error: fail_on_error
42
49
  )
50
+ return false unless downloaded
51
+
52
+ temporary_file.flush
53
+ # Do a quick check of the downloaded `.strings` file to ensure it looks valid
54
+ validate_strings_file(temporary_file.path, display_path: destination, fail_on_error: fail_on_error) unless skip_file_validation
55
+ File.chmod(destination_mode, temporary_file.path)
56
+ temporary_file.close
57
+ File.rename(temporary_file.path, destination)
43
58
  end
59
+ true
60
+ rescue FastlaneCore::Interface::FastlaneError
61
+ raise
44
62
  rescue StandardError => e
45
- UI.error("Error while validating the file exported from GlotPress (`#{destination}`) - #{e.message.chomp}")
63
+ report_error("Error writing downloaded locale `#{locale}` — #{e.message} (#{destination})", fail_on_error: fail_on_error)
64
+ false
65
+ end
66
+ private_class_method :download_and_replace_strings_file
67
+
68
+ # Validate that a `.strings` file downloaded from GlotPress seems valid and does not contain empty translations
69
+ def self.validate_strings_file(path, display_path: path, fail_on_error: false)
70
+ unless File.exist?(path)
71
+ report_error("The file exported from GlotPress was not created (`#{display_path}`)", fail_on_error: fail_on_error) if fail_on_error
72
+ return
73
+ end
74
+
75
+ if File.empty?(path)
76
+ report_error("The file exported from GlotPress is empty (`#{display_path}`)", fail_on_error: fail_on_error) if fail_on_error
77
+ return
78
+ end
79
+
80
+ translations = nil
81
+ begin
82
+ translations = Fastlane::Helper::Ios::L10nHelper.read_strings_file_as_hash(path: path)
83
+ rescue StandardError => e
84
+ report_error("Error while validating the file exported from GlotPress (`#{display_path}`) - #{e.message.chomp}", fail_on_error: fail_on_error)
85
+ return
86
+ end
87
+
88
+ unless translations.is_a?(Hash) && translations.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
89
+ report_error("The file exported from GlotPress is not a string-to-string dictionary (`#{display_path}`)", fail_on_error: fail_on_error)
90
+ return
91
+ end
92
+
93
+ empty_keys = translations.select { |_, value| value.nil? || value.empty? }.keys.sort
94
+ return if empty_keys.empty?
95
+
96
+ report_error(
97
+ "Found empty translations in `#{display_path}` for the following keys: #{empty_keys.inspect}.\n" \
98
+ + "This is likely a GlotPress bug, and will lead to copies replaced by empty text in the UI.\n" \
99
+ + 'Please report this to the GlotPress team, and fix the file locally before continuing.',
100
+ fail_on_error: fail_on_error
101
+ )
102
+ end
103
+
104
+ def self.report_error(message, fail_on_error:)
105
+ fail_on_error ? UI.user_error!(message) : UI.error(message)
46
106
  end
107
+ private_class_method :report_error
47
108
 
48
109
  #####################################################
49
110
  # @!group Documentation
@@ -92,6 +153,7 @@ module Fastlane
92
153
  type: Fastlane::Boolean,
93
154
  optional: true,
94
155
  default_value: false),
156
+ Fastlane::Helper::ConfigItemHelper.opt_in_fail_on_error_config_item,
95
157
  ]
96
158
  end
97
159
 
@@ -243,9 +243,11 @@ module Fastlane
243
243
  # @param [Array<Hash{Symbol=>String}>] locales_map
244
244
  # An array of locales to download. Each item in the array must be a Hash
245
245
  # with keys `:glotpress` and `:android` containing the respective locale codes.
246
+ # @param [Boolean] fail_on_error Whether to fail on request errors or invalid downloaded XML.
246
247
  #
247
- def self.download_from_glotpress(res_dir:, glotpress_project_url:, locales_map:, glotpress_filters: { status: 'current' })
248
+ def self.download_from_glotpress(res_dir:, glotpress_project_url:, locales_map:, glotpress_filters: { status: 'current' }, fail_on_error: false)
248
249
  glotpress_filters = [glotpress_filters] unless glotpress_filters.is_a?(Array)
250
+ UI.user_error!('At least one GlotPress filter is required.') if fail_on_error && glotpress_filters.empty?
249
251
 
250
252
  orig_file = File.join(res_dir, 'values', 'strings.xml')
251
253
  orig_xml = File.open(orig_file) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }
@@ -253,7 +255,7 @@ module Fastlane
253
255
  locales_map.each do |lang_codes|
254
256
  all_xml_documents = glotpress_filters.map do |filters|
255
257
  UI.message "Downloading translations for '#{lang_codes[:android]}' from GlotPress (#{lang_codes[:glotpress]}) [#{filters}]..."
256
- download_glotpress_export_file(project_url: glotpress_project_url, locale: lang_codes[:glotpress], filters: filters)
258
+ download_glotpress_export_file(project_url: glotpress_project_url, locale: lang_codes[:glotpress], filters: filters, fail_on_error: fail_on_error)
257
259
  end.compact
258
260
  next if all_xml_documents.empty?
259
261
 
@@ -282,19 +284,35 @@ module Fastlane
282
284
  # @param [String] locale The GlotPress locale code to download strings for.
283
285
  # @param [Hash{Symbol=>String}] filters The hash of filters to apply when exporting from GlotPress.
284
286
  # Typical examples include `{ status: 'current' }` or `{ status: 'review' }`.
287
+ # @param [Boolean] fail_on_error Whether to fail on request errors or invalid downloaded XML.
285
288
  # @return [Nokogiri::XML::Document] the download XML document, parsed as a Nokogiri::XML object
286
289
  #
287
- def self.download_glotpress_export_file(project_url:, locale:, filters:)
290
+ def self.download_glotpress_export_file(project_url:, locale:, filters:, fail_on_error:)
288
291
  query_params = filters.transform_keys { |k| "filters[#{k}]" }.merge(format: 'android')
289
292
  url = "#{project_url.chomp('/')}/#{locale}/default/export-translations/?#{URI.encode_www_form(query_params)}"
290
293
 
291
294
  Fastlane::Helper::GlotPressDownloader.download(
292
295
  url: url,
293
296
  locale: locale,
294
- auto_retry: true
297
+ auto_retry: true,
298
+ fail_on_error: fail_on_error
295
299
  ) do |response_body|
296
300
  # Replace tabs with spaces (GlotPress uses tabs, but we prefer spaces)
297
- Nokogiri::XML(response_body.gsub("\t", ' '), nil, Encoding::UTF_8.to_s)
301
+ if fail_on_error
302
+ begin
303
+ xml = Nokogiri::XML(response_body.gsub("\t", ' '), nil, Encoding::UTF_8.to_s, &:strict)
304
+ rescue Nokogiri::XML::SyntaxError => e
305
+ UI.user_error!("Invalid Android translation export for locale `#{locale}` — #{e.message} (#{url})")
306
+ end
307
+
308
+ unless xml.root&.name == 'resources'
309
+ UI.user_error!("Invalid Android translation export for locale `#{locale}` — expected a `resources` root element (#{url})")
310
+ end
311
+
312
+ xml
313
+ else
314
+ Nokogiri::XML(response_body.gsub("\t", ' '), nil, Encoding::UTF_8.to_s)
315
+ end
298
316
  end
299
317
  end
300
318
  private_class_method :download_glotpress_export_file
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fastlane
4
+ module Helper
5
+ class ConfigItemHelper
6
+ def self.opt_in_fail_on_error_config_item
7
+ FastlaneCore::ConfigItem.new(
8
+ key: :fail_on_error,
9
+ description: 'Whether handled errors should fail the lane instead of using the action-specific fallback behavior',
10
+ type: FastlaneCore::Boolean,
11
+ optional: true,
12
+ default_value: false
13
+ )
14
+ end
15
+ end
16
+ end
17
+ end
@@ -9,19 +9,22 @@ module Fastlane
9
9
  class GlotPressDownloader
10
10
  AUTO_RETRY_SLEEP_TIME = 20
11
11
  MAX_AUTO_RETRY_ATTEMPTS = 30
12
+ MAX_REDIRECTS = 10
12
13
 
13
- attr_reader :auto_retry, :auto_retry_attempt_counter, :url, :locale
14
+ attr_reader :auto_retry, :auto_retry_attempt_counter, :url, :locale, :fail_on_error
14
15
 
15
16
  # Initialize a new GlotPressDownloader
16
17
  #
17
18
  # @param [String] url The URL to download from
18
19
  # @param [String] locale The locale being downloaded (for logging purposes)
19
20
  # @param [Boolean] auto_retry Whether to automatically retry on rate limiting (429 errors)
21
+ # @param [Boolean] fail_on_error Whether to raise a Fastlane error after retry handling instead of returning false
20
22
  #
21
- def initialize(url:, locale:, auto_retry: false)
23
+ def initialize(url:, locale:, auto_retry: false, fail_on_error: false)
22
24
  @url = url
23
25
  @locale = locale
24
26
  @auto_retry = auto_retry
27
+ @fail_on_error = fail_on_error
25
28
  @auto_retry_attempt_counter = 0
26
29
  end
27
30
 
@@ -30,34 +33,55 @@ module Fastlane
30
33
  # @param [String] url The URL to download from
31
34
  # @param [String] locale The locale being downloaded (for logging purposes)
32
35
  # @param [Boolean] auto_retry Whether to automatically retry on rate limiting (429 errors)
33
- # @yield [String] The response body if the download was successful
34
- # @return The result of the block if provided, or true/false indicating success if no block provided
36
+ # @param [Boolean] fail_on_error Whether to raise a Fastlane error after retry handling instead of returning false
37
+ # @yield [String] The response body if the download was successful. Return `false` to reject it and suppress the success message.
38
+ # @return The result of the block if provided, or true/false indicating success if no block is provided
39
+ # @raise [FastlaneCore::Interface::FastlaneError] If the download fails after retry handling and `fail_on_error` is true
35
40
  #
36
- #
37
- def self.download(url:, locale:, auto_retry: false, &)
38
- new(url: url, locale: locale, auto_retry: auto_retry).download(&)
41
+ def self.download(url:, locale:, auto_retry: false, fail_on_error: false, &)
42
+ new(url: url, locale: locale, auto_retry: auto_retry, fail_on_error: fail_on_error).download(&)
39
43
  end
40
44
 
41
45
  # Downloads data from GlotPress
42
46
  #
43
- # @yield [String] The response body if the download was successful
44
- # @return The result of the block if provided, or true/false indicating success if no block provided
47
+ # @yield [String] The response body if the download was successful. Return `false` to reject it and suppress the success message.
48
+ # @return The result of the block if provided, or true/false indicating success if no block is provided
49
+ # @raise [FastlaneCore::Interface::FastlaneError] If the download fails after retry handling and `fail_on_error` is true
45
50
  #
46
51
  def download(&)
47
52
  @auto_retry_attempt_counter = 0 # Reset counter only at start of download
48
- download_from_url(@url, &)
53
+ download_from_url(@url, redirect_count: 0, &)
49
54
  end
50
55
 
51
56
  private
52
57
 
53
- def download_from_url(url, &)
54
- uri = URI(url)
58
+ def download_from_url(url, redirect_count:, &)
59
+ uri = parse_uri(url)
60
+ return block_given? ? nil : false if uri.nil?
61
+
55
62
  response = make_request(uri)
63
+ return block_given? ? nil : false if response.nil?
64
+
65
+ unless block_given?
66
+ return handle_response(response: response, url: url, original_uri: uri, redirect_count: redirect_count)
67
+ end
68
+
56
69
  result = nil
57
- success = handle_response(response: response, url: url, original_uri: uri) do |body|
58
- result = yield body if block_given?
70
+ handle_response(response: response, url: url, original_uri: uri, redirect_count: redirect_count) do |body|
71
+ result = yield(body)
59
72
  end
60
- block_given? ? result : success
73
+ result
74
+ end
75
+
76
+ def parse_uri(url)
77
+ uri = URI(url)
78
+ return uri if uri.is_a?(URI::HTTP) && uri.host
79
+
80
+ handle_failure("Invalid URL for locale `#{@locale}` (#{url})")
81
+ nil
82
+ rescue URI::InvalidURIError, TypeError => e
83
+ handle_failure("Invalid URL for locale `#{@locale}` — #{e.message} (#{url})")
84
+ nil
61
85
  end
62
86
 
63
87
  def make_request(uri)
@@ -68,60 +92,70 @@ module Fastlane
68
92
  http.request(request)
69
93
  rescue StandardError => e
70
94
  # Network errors, connection errors, etc.
71
- UI.error("Error downloading locale `#{@locale}` — #{e.message} (#{uri})")
95
+ message = "Error downloading locale `#{@locale}` — #{e.message} (#{uri})"
72
96
  retry if UI.interactive? && UI.confirm("Retry downloading `#{@locale}`?")
97
+ handle_failure(message)
73
98
  nil
74
99
  end
75
100
 
76
- def handle_response(response:, url:, original_uri:)
77
- return false if response.nil?
78
-
101
+ def handle_response(response:, url:, original_uri:, redirect_count:, &)
79
102
  case response.code
80
103
  when '200'
104
+ accepted = block_given? ? yield(response.body) : true
105
+ return false if accepted == false
106
+
81
107
  UI.success("Successfully downloaded `#{@locale}`.")
82
- yield response.body if block_given?
83
108
  true
84
109
  when '301', '302', '307', '308'
85
110
  # Follow the redirect
86
111
  UI.message("Received #{response.code} for `#{@locale}`. Following redirect...")
87
112
  redirect_url = response['location']
88
- if redirect_url.nil?
89
- UI.error("Received #{response.code} but no location header found.")
90
- false
91
- else
92
- # Follow redirect with the new URL
93
- download_from_url(redirect_url) { |body| yield body if block_given? }
94
- end
113
+ return handle_failure("Received #{response.code} for `#{@locale}` but no location header was found (#{original_uri}).") if redirect_url.nil? || redirect_url.empty?
114
+ return handle_failure("Too many redirects while downloading locale `#{@locale}` (#{original_uri}).") if redirect_count >= MAX_REDIRECTS
115
+
116
+ resolved_url = resolve_redirect_url(original_uri, redirect_url)
117
+ return false if resolved_url.nil?
118
+
119
+ download_from_url(resolved_url, redirect_count: redirect_count + 1, &)
95
120
  when '429'
96
121
  # Rate limited
97
- handle_rate_limiting(url: url) do |body|
98
- yield body if block_given?
99
- end
122
+ handle_rate_limiting(url: url, response: response, redirect_count: redirect_count, &)
100
123
  else
101
124
  # Unexpected status code (including 404, 500, etc.)
102
- status_line = "#{response.code} #{response.message}"
103
- UI.error("Error downloading locale `#{@locale}` — #{status_line} (#{original_uri})")
104
- if UI.interactive? && UI.confirm("Retry downloading `#{@locale}`?")
105
- download_from_url(url) { |body| yield body if block_given? }
106
- else
107
- false
108
- end
125
+ status_line = [response.code, response.message].compact.join(' ').strip
126
+ message = "Error downloading locale `#{@locale}` — #{status_line} (#{original_uri})"
127
+ return handle_failure(message) unless UI.interactive? && UI.confirm("Retry downloading `#{@locale}`?")
128
+
129
+ download_from_url(url, redirect_count: redirect_count, &)
109
130
  end
110
131
  end
111
132
 
112
- def handle_rate_limiting(url:)
133
+ def handle_rate_limiting(url:, response:, redirect_count:, &)
113
134
  if @auto_retry && @auto_retry_attempt_counter < MAX_AUTO_RETRY_ATTEMPTS
114
135
  UI.message("Received 429 for `#{@locale}`. Auto retrying in #{AUTO_RETRY_SLEEP_TIME} seconds... (attempt #{@auto_retry_attempt_counter + 1}/#{MAX_AUTO_RETRY_ATTEMPTS})")
115
136
  sleep(AUTO_RETRY_SLEEP_TIME)
116
137
  @auto_retry_attempt_counter += 1
117
- download_from_url(url) { |body| yield body if block_given? }
138
+ download_from_url(url, redirect_count: redirect_count, &)
118
139
  elsif UI.interactive? && UI.confirm("Retry downloading `#{@locale}` after receiving 429 from the API?")
119
- download_from_url(url) { |body| yield body if block_given? }
140
+ download_from_url(url, redirect_count: redirect_count, &)
120
141
  else
121
- UI.error("Abandoning `#{@locale}` download.")
122
- false
142
+ status_line = [response.code, response.message].compact.join(' ').strip
143
+ handle_failure("Error downloading locale `#{@locale}` — #{status_line} (#{url})")
123
144
  end
124
145
  end
146
+
147
+ def resolve_redirect_url(original_uri, redirect_url)
148
+ URI.join(original_uri.to_s, redirect_url).to_s
149
+ rescue URI::InvalidURIError => e
150
+ handle_failure("Invalid redirect URL for locale `#{@locale}` — #{e.message} (#{redirect_url})")
151
+ nil
152
+ end
153
+
154
+ def handle_failure(message)
155
+ UI.error(message)
156
+ UI.user_error!(message) if @fail_on_error
157
+ false
158
+ end
125
159
  end
126
160
  end
127
161
  end
@@ -191,26 +191,27 @@ module Fastlane
191
191
  # @param [Hash{Symbol=>String}] filters The hash of filters to apply when exporting from GlotPress.
192
192
  # Typical examples include `{ status: 'current' }` or `{ status: 'review' }`.
193
193
  # @param [String, IO] destination The path or `IO`-like instance, where to write the downloaded file on disk.
194
+ # @param [Boolean] fail_on_error Whether to fail on request errors.
194
195
  #
195
- def self.download_glotpress_export_file(project_url:, locale:, filters:, destination:)
196
+ def self.download_glotpress_export_file(project_url:, locale:, filters:, destination:, fail_on_error: false)
196
197
  query_params = (filters || {}).transform_keys { |k| "filters[#{k}]" }.merge(format: 'strings')
197
198
  url = "#{project_url.chomp('/')}/#{locale}/default/export-translations/?#{URI.encode_www_form(query_params)}"
198
199
 
199
- begin
200
- Fastlane::Helper::GlotPressDownloader.download(
201
- url: url,
202
- locale: locale,
203
- auto_retry: true
204
- ) do |response_body|
205
- if destination.is_a?(String)
206
- File.write(destination, response_body)
207
- else
208
- destination.write(response_body)
209
- end
200
+ Fastlane::Helper::GlotPressDownloader.download(
201
+ url: url,
202
+ locale: locale,
203
+ auto_retry: true,
204
+ fail_on_error: fail_on_error
205
+ ) do |response_body|
206
+ if destination.is_a?(String)
207
+ File.write(destination, response_body)
208
+ else
209
+ destination.write(response_body)
210
210
  end
211
211
  rescue StandardError => e
212
- UI.error "Error downloading locale `#{locale}` — #{e.message} (#{url})"
213
- nil
212
+ message = "Error writing downloaded locale `#{locale}` — #{e.message} (#{url})"
213
+ fail_on_error ? UI.user_error!(message) : UI.error(message)
214
+ false
214
215
  end
215
216
  end
216
217
  end
@@ -9,10 +9,11 @@ module Fastlane
9
9
  class MetadataDownloader
10
10
  attr_reader :target_folder, :target_files
11
11
 
12
- def initialize(target_folder, target_files, auto_retry)
12
+ def initialize(target_folder, target_files, auto_retry, fail_on_error: false)
13
13
  @target_folder = target_folder
14
14
  @target_files = target_files
15
15
  @auto_retry = auto_retry
16
+ @fail_on_error = fail_on_error
16
17
  @alternates = {}
17
18
  end
18
19
 
@@ -21,9 +22,10 @@ module Fastlane
21
22
  GlotPressDownloader.download(
22
23
  url: glotpress_url,
23
24
  locale: target_locale,
24
- auto_retry: @auto_retry
25
+ auto_retry: @auto_retry,
26
+ fail_on_error: @fail_on_error
25
27
  ) do |response_body|
26
- handle_glotpress_response(response_body: response_body, locale: target_locale, is_source: is_source)
28
+ handle_glotpress_response(response_body: response_body, locale: target_locale, is_source: is_source, url: glotpress_url)
27
29
  end
28
30
  end
29
31
 
@@ -106,17 +108,34 @@ module Fastlane
106
108
 
107
109
  private
108
110
 
109
- def handle_glotpress_response(response_body:, locale:, is_source:)
111
+ def handle_glotpress_response(response_body:, locale:, is_source:, url:)
110
112
  # Parse the JSON response
111
113
  @alternates.clear
112
- loc_data = begin
113
- JSON.parse(response_body)
114
- rescue StandardError
115
- nil
116
- end
114
+ loc_data = parse_metadata_response(response_body: response_body, locale: locale, url: url)
115
+
117
116
  parse_data(locale, loc_data, is_source)
118
117
  reparse_alternates(locale, loc_data, is_source) unless @alternates.empty?
119
118
  end
119
+
120
+ def parse_metadata_response(response_body:, locale:, url:)
121
+ return JSON.parse(response_body) unless @fail_on_error
122
+
123
+ loc_data = JSON.parse(response_body)
124
+ UI.user_error!("Unexpected GlotPress metadata response for locale `#{locale}` (#{url})") unless valid_metadata_response?(loc_data)
125
+ loc_data
126
+ rescue JSON::ParserError, TypeError => e
127
+ UI.user_error!("Error parsing GlotPress response for locale `#{locale}` — #{e.message} (#{url})") if @fail_on_error
128
+ nil
129
+ end
130
+
131
+ def valid_metadata_response?(loc_data)
132
+ return loc_data.empty? if loc_data.is_a?(Array)
133
+ return false unless loc_data.is_a?(Hash) && !loc_data.empty?
134
+
135
+ loc_data.all? do |source, translations|
136
+ source.is_a?(String) && translations.is_a?(Array) && translations.all?(String)
137
+ end
138
+ end
120
139
  end
121
140
  end
122
141
  end
@@ -3,6 +3,6 @@
3
3
  module Fastlane
4
4
  module Wpmreleasetoolkit
5
5
  NAME = 'fastlane-plugin-wpmreleasetoolkit'
6
- VERSION = '15.0.0'
6
+ VERSION = '15.1.0'
7
7
  end
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-wpmreleasetoolkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 15.0.0
4
+ version: 15.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Automattic
@@ -71,14 +71,14 @@ dependencies:
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: '2.237'
74
+ version: '2.240'
75
75
  type: :runtime
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
- version: '2.237'
81
+ version: '2.240'
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: gettext
84
84
  requirement: !ruby/object:Gem::Requirement
@@ -343,6 +343,7 @@ files:
343
343
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/apps_cdn_helper.rb
344
344
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/buildkite_aware_log_groups.rb
345
345
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/ci_helper.rb
346
+ - lib/fastlane/plugin/wpmreleasetoolkit/helper/config_item_helper.rb
346
347
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/configure_helper.rb
347
348
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/encryption_helper.rb
348
349
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/filesystem_helper.rb