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 +4 -4
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_download_translations_action.rb +5 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/get_prs_between_tags.rb +2 -9
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/gp_downloadmetadata_action.rb +4 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_download_strings_files_from_glotpress.rb +77 -15
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb +23 -5
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/config_item_helper.rb +17 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/glotpress_downloader.rb +76 -42
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb +15 -14
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/metadata_download_helper.rb +28 -9
- data/lib/fastlane/plugin/wpmreleasetoolkit/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f47aea4602fae982122ed3c370c7cbc3330ebe242781ff4abffb4498016f384c
|
|
4
|
+
data.tar.gz: e4526ac1fcbb27ece5e15fc8a11ea60d453c2bbd1ab34a8f1c88040f590e31f8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 297833bfc56d46af40b9771485fe42cc5b2e54c910ddf92ea2701e50088bb4015176ed988544e593128569e3f410093cb0421976b32167e4c9755c423fc44c0d
|
|
7
|
+
data.tar.gz: 39d1cd8e4a1cc8fde878d10538cc5a49cb6b2fa58ad2f2a80fb3d1c2ea4fa830c8e4803f7052fdbd243619b4dcc60a150f4b522504f218327dd4145f3b15dee5
|
data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_download_translations_action.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_download_strings_files_from_glotpress.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
# @
|
|
34
|
-
# @
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
58
|
-
result = yield
|
|
70
|
+
handle_response(response: response, url: url, original_uri: uri, redirect_count: redirect_count) do |body|
|
|
71
|
+
result = yield(body)
|
|
59
72
|
end
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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 =
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
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
|
|
140
|
+
download_from_url(url, redirect_count: redirect_count, &)
|
|
120
141
|
else
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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 =
|
|
113
|
-
|
|
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
|
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.
|
|
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.
|
|
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.
|
|
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
|