fastlane-plugin-wpmreleasetoolkit 14.11.3 → 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: 07aea16b1fb8656819d8dd16269523c9ba32a6fdd6fa6c60caa2765119d80c02
4
- data.tar.gz: 1058fad8effd34bf07937e702748a934d2420cf0f171ef8a21dc1de5fe03447e
3
+ metadata.gz: f47aea4602fae982122ed3c370c7cbc3330ebe242781ff4abffb4498016f384c
4
+ data.tar.gz: e4526ac1fcbb27ece5e15fc8a11ea60d453c2bbd1ab34a8f1c88040f590e31f8
5
5
  SHA512:
6
- metadata.gz: e121905842c5f0226f8a7c2d35ac4a3dc5386d93650d96347a8862cd31964807f58fb1cdc62225c365e59ab7071b96ccdbb4199e96433ba4955d1054aa5227a1
7
- data.tar.gz: 709be6a17688a4cd77e63d9822671b8edabe163091024e40c44ef12ac4e194294f93226eee812bee27e19eb1c3fea714e194afdb73221513351fbb994262fcc0
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
@@ -25,6 +26,8 @@ module Fastlane
25
26
  config_file_path: config_file_path
26
27
  )
27
28
  rescue StandardError => e
29
+ raise if params[:fail_on_error]
30
+
28
31
  error_msg = "❌ Error computing the list of PRs since #{previous_tag || 'last release'}: `#{e.message}`"
29
32
  UI.important(error_msg)
30
33
  error_msg # Use error message as GitHub Release body to help us be aware of what went wrong.
@@ -102,6 +105,7 @@ module Fastlane
102
105
  'See https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes#configuration-options',
103
106
  optional: true,
104
107
  type: String),
108
+ Fastlane::Helper::ConfigItemHelper.opt_in_fail_on_error_config_item,
105
109
  Fastlane::Helper::GithubHelper.github_token_config_item,
106
110
  ]
107
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
 
@@ -15,10 +15,22 @@ module Fastlane
15
15
  end
16
16
 
17
17
  # Set up by loading the .env file with the given name.
18
+ #
19
+ # When `mutate_env` is true (the default), values from the .env file are
20
+ # layered into the process `ENV` using no-override semantics: keys already
21
+ # set in `ENV` (e.g. by CI) win. This lets fastlane actions that look up
22
+ # values via `ENV.fetch(...)` for their `default_value:` find them without
23
+ # the caller having to thread them through explicitly.
24
+ #
25
+ # Pass `mutate_env: false` to keep `ENV` pristine — values are still
26
+ # accessible via `get_required_env!`, but only through this instance.
27
+ # Useful for tests that want isolation, or for callers that prefer to
28
+ # control `ENV` themselves.
18
29
  def initialize(
19
30
  env_file_name:,
20
31
  env_file_folder: File.join(Dir.home, '.a8c-apps'),
21
32
  example_env_file_path: 'fastlane/example.env',
33
+ mutate_env: true,
22
34
  print_error_lambda: ->(message) { FastlaneCore::UI.user_error!(message) },
23
35
  print_warning_lambda: ->(message) { FastlaneCore::UI.important(message) }
24
36
  )
@@ -31,10 +43,30 @@ module Fastlane
31
43
  @print_warning_lambda.call("Warning: env file not found at #{@env_path}. Environment variables may not be loaded.")
32
44
  end
33
45
 
34
- # Parse rather than load so we don't mutate the global ENV. Each instance
35
- # gets its own view, and values from the process environment (e.g. set by
36
- # CI) still take precedence — see `env_value`.
37
- @loaded_env = File.exist?(@env_path) ? Dotenv.parse(@env_path) : {}
46
+ unless mutate_env
47
+ @loaded_env = File.exist?(@env_path) ? Dotenv.parse(@env_path) : {}
48
+ @mutations = {}
49
+ return
50
+ end
51
+
52
+ # `load` rather than `parse` so interpolated values (`B=${A}`) resolve
53
+ # against the process `ENV`; `parse` resolves them against the file's own values.
54
+ env_before = ENV.to_h
55
+ @loaded_env = File.exist?(@env_path) ? Dotenv.load(@env_path) : {}
56
+ # Records the value this instance wrote for each key it added. On restore,
57
+ # we delete only if `ENV[key]` still matches — if a later caller overwrote
58
+ # it, their value is left alone.
59
+ @mutations = ENV.to_h.reject { |key, _| env_before.key?(key) }
60
+ end
61
+
62
+ # Remove from `ENV` any keys this instance added via `mutate_env: true`,
63
+ # but only if the value is still the one we wrote. Keys that a later
64
+ # caller has overwritten are left untouched.
65
+ # Idempotent — calling more than once is safe. Used by `reset!` and
66
+ # available for callers that want to roll back manually.
67
+ def restore_env!
68
+ @mutations.each { |key, value| ENV.delete(key) if ENV[key] == value }
69
+ @mutations = {}
38
70
  end
39
71
 
40
72
  # Use this instead of getting values from `ENV` directly. It will throw an error if the requested value is missing or empty.
@@ -135,8 +167,10 @@ module Fastlane
135
167
  default!.require_env_vars!(*keys)
136
168
  end
137
169
 
138
- # Clears the default instance, useful for test teardown.
170
+ # Clears the default instance, useful for test teardown. Also rolls back
171
+ # any `ENV` mutations the default instance performed via `mutate_env`.
139
172
  def self.reset!
173
+ @default&.restore_env!
140
174
  @default = nil
141
175
  end
142
176
 
@@ -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 = '14.11.3'
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: 14.11.3
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
@@ -271,7 +271,6 @@ files:
271
271
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_localize_libs_action.rb
272
272
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_update_metadata_source_action.rb
273
273
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_validate_lib_strings_action.rb
274
- - lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_build_preflight.rb
275
274
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_create_avd_action.rb
276
275
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_create_xml_release_notes.rb
277
276
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_current_branch_is_hotfix.rb
@@ -324,7 +323,6 @@ files:
324
323
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_validate_action.rb
325
324
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/add_development_certificates_to_provisioning_profiles.rb
326
325
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/add_devices_to_provisioning_profiles.rb
327
- - lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_build_preflight.rb
328
326
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_check_beta_deps.rb
329
327
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_download_strings_files_from_glotpress.rb
330
328
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_extract_keys_from_strings_files.rb
@@ -345,6 +343,7 @@ files:
345
343
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/apps_cdn_helper.rb
346
344
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/buildkite_aware_log_groups.rb
347
345
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/ci_helper.rb
346
+ - lib/fastlane/plugin/wpmreleasetoolkit/helper/config_item_helper.rb
348
347
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/configure_helper.rb
349
348
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/encryption_helper.rb
350
349
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/filesystem_helper.rb
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fastlane
4
- module Actions
5
- class AndroidBuildPreflightAction < Action
6
- def self.run(params)
7
- # Validate mobile configuration secrets
8
- other_action.configure_apply
9
-
10
- # Check gems and pods are up to date. This will exit if it fails
11
- begin
12
- Action.sh('bundle check')
13
- rescue StandardError
14
- UI.user_error!("You should run 'bundle install' to make sure gems are up to date")
15
- raise
16
- end
17
-
18
- begin
19
- Action.sh('command -v bundletool > /dev/null')
20
- rescue StandardError
21
- UI.user_error!("bundletool is required to build the APKs. Install it with 'brew install bundletool'")
22
- raise
23
- end
24
- end
25
-
26
- #####################################################
27
- # @!group Documentation
28
- #####################################################
29
-
30
- def self.description
31
- 'Clean the environment to ensure a safe build'
32
- end
33
-
34
- def self.details
35
- 'Clean the environment to ensure a safe build'
36
- end
37
-
38
- def self.available_options
39
- end
40
-
41
- def self.output
42
- end
43
-
44
- def self.return_value
45
- end
46
-
47
- def self.authors
48
- ['Automattic']
49
- end
50
-
51
- def self.is_supported?(platform)
52
- platform == :android
53
- end
54
- end
55
- end
56
- end
@@ -1,79 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fastlane
4
- module Actions
5
- class IosBuildPreflightAction < Action
6
- def self.run(params)
7
- # Validate mobile configuration secrets
8
- other_action.configure_apply
9
-
10
- Action.sh("cd .. && rm -rf #{params[:derived_data_path]}")
11
-
12
- # Verify that ImageMagick exists on this machine and can be called from the command-line.
13
- # Internal Builds use it to generate the App Icon as part of the build process
14
- begin
15
- Action.sh('which convert')
16
- rescue StandardError
17
- UI.user_error!("Couldn't find ImageMagick. Please install it by running `brew install imagemagick`")
18
- raise
19
- end
20
-
21
- # Verify that Ghostscript exists on this machine and can be called from the command-line.
22
- # Internal Builds use it to generate the App Icon as part of the build process
23
- begin
24
- Action.sh('which gs')
25
- rescue StandardError
26
- UI.user_error!("Couldn't find Ghostscript. Please install it by running `brew install ghostscript`")
27
- raise
28
- end
29
-
30
- # Check gems and pods are up to date. This will exit if it fails
31
- begin
32
- Action.sh('bundle check')
33
- rescue StandardError
34
- UI.user_error!("You should run 'rake dependencies' to make sure gems are up to date")
35
- raise
36
- end
37
-
38
- other_action.cocoapods
39
- end
40
-
41
- #####################################################
42
- # @!group Documentation
43
- #####################################################
44
-
45
- def self.description
46
- 'Clean the environment to ensure a safe build'
47
- end
48
-
49
- def self.details
50
- 'Clean the environment to ensure a safe build'
51
- end
52
-
53
- def self.available_options
54
- [
55
- FastlaneCore::ConfigItem.new(
56
- key: :derived_data_path,
57
- description: "The path to the DerivedData directory for the project. Should match what's used in the `gym` action",
58
- type: String,
59
- default_value: '~/Library/Developer/Xcode/DerivedData'
60
- ),
61
- ]
62
- end
63
-
64
- def self.output
65
- end
66
-
67
- def self.return_value
68
- end
69
-
70
- def self.authors
71
- ['Automattic']
72
- end
73
-
74
- def self.is_supported?(platform)
75
- %i[ios mac].include?(platform)
76
- end
77
- end
78
- end
79
- end