fastlane-plugin-wpmreleasetoolkit 4.0.0 → 5.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_localize_libs_action.rb +10 -5
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_update_metadata_source_action.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_validate_lib_strings_action.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_betabuild_prechecks.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_build_prechecks.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_build_preflight.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_beta.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_final_release.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_hotfix.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_release.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_codefreeze_prechecks.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_completecodefreeze_prechecks.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_create_xml_release_notes.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_current_branch_is_hotfix.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_download_file_by_version.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_download_translations_action.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_finalize_prechecks.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_firebase_test.rb +187 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_alpha_version.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_app_version.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_release_version.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/{android_hotifx_prechecks.rb → android_hotfix_prechecks.rb} +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb +279 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_tag_build.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_update_release_notes.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/check_for_toolkit_updates_action.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/check_translation_progress.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/circleci_trigger_job_action.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/close_milestone_action.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_new_milestone_action.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_release_action.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/extract_release_notes_for_version_action.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/firebase_login.rb +44 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/get_prs_list_action.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/gp_downloadmetadata_action.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/gp_update_metadata_source.rb +1 -2
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/promo_screenshots_action.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/removebranchprotection_action.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/setbranchprotection_action.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/setfrozentag_action.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_add_files_to_copy_action.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_apply_action.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_download_action.rb +2 -3
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_setup_action.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_update_action.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_validate_action.rb +3 -3
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/add_development_certificates_to_provisioning_profiles.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/add_devices_to_provisioning_profiles.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_betabuild_prechecks.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_build_prechecks.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_build_preflight.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_bump_version_beta.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_bump_version_hotfix.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_bump_version_release.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_check_beta_deps.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_clear_intermediate_tags.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_codefreeze_prechecks.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_completecodefreeze_prechecks.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_current_branch_is_hotfix.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_final_tag.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_finalize_prechecks.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_app_version.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_build_version.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_store_app_sizes.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/{ios_hotifx_prechecks.rb → ios_hotfix_prechecks.rb} +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_lint_localizations.rb +48 -9
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_localize_project.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_merge_strings_files.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb +170 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_tag_build.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_metadata.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_metadata_source.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_release_notes.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_validate_ci_build.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb +72 -49
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb +95 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/git_helper.rb +10 -18
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb +7 -3
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_linter_helper.rb +20 -34
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_strings_file_validation_helper.rb +107 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/metadata_download_helper.rb +3 -5
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/promo_screenshots_helper.rb +2 -3
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/release_notes_helper.rb +4 -2
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/user_agent.rb +5 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/models/file_reference.rb +2 -4
- data/lib/fastlane/plugin/wpmreleasetoolkit/models/firebase_account.rb +19 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/models/firebase_device.rb +62 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/models/firebase_test_lab_result.rb +36 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/models/firebase_test_runner.rb +104 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/version.rb +1 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit.rb +2 -2
- metadata +122 -83
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'net/http'
|
3
|
+
require 'zlib'
|
4
|
+
|
5
|
+
module Fastlane
|
6
|
+
module Helper
|
7
|
+
# A helper class to build an App Size Metrics payload and send it to a server (or write it to disk)
|
8
|
+
#
|
9
|
+
# The payload generated (and sent) by this helper conforms to the API for grouped metrics described in
|
10
|
+
# https://github.com/Automattic/apps-metrics
|
11
|
+
#
|
12
|
+
class AppSizeMetricsHelper
|
13
|
+
# @param [Hash] metadata Metadata common to all the metrics. Can be any arbitrary set of key/value pairs.
|
14
|
+
#
|
15
|
+
def initialize(metadata = {})
|
16
|
+
self.metadata = metadata
|
17
|
+
@metrics = []
|
18
|
+
end
|
19
|
+
|
20
|
+
# Sets the metadata common to the whole group of metrics in the payload being built by this helper instance
|
21
|
+
#
|
22
|
+
# @param [Hash] hash The metadata common to all the metrics of the payload built by that helper instance. Can be any arbitrary set of key/value pairs
|
23
|
+
#
|
24
|
+
def metadata=(hash)
|
25
|
+
@metadata = (hash.compact || {}).map { |key, value| { name: key.to_s, value: value } }
|
26
|
+
end
|
27
|
+
|
28
|
+
# Adds a single metric to the group of metrics
|
29
|
+
#
|
30
|
+
# @param [String] name The metric name
|
31
|
+
# @param [Integer] value The metric value
|
32
|
+
# @param [Hash] metadata The arbitrary dictionary of metadata to associate to that metric entry
|
33
|
+
#
|
34
|
+
def add_metric(name:, value:, metadata: nil)
|
35
|
+
metric = { name: name, value: value }
|
36
|
+
metadata = metadata&.compact || {} # Remove nil values if any (and use empty Hash if nil was provided)
|
37
|
+
metric[:meta] = metadata.map { |meta_key, meta_value| { name: meta_key.to_s, value: meta_value } } unless metadata.empty?
|
38
|
+
@metrics.append(metric)
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_h
|
42
|
+
{
|
43
|
+
meta: @metadata,
|
44
|
+
metrics: @metrics
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
# Send the metrics to the given App Metrics endpoint.
|
49
|
+
#
|
50
|
+
# Must conform to the API described in https://github.com/Automattic/apps-metrics/wiki/Queue-Group-of-Metrics
|
51
|
+
#
|
52
|
+
# @param [String,URI] to The URL of the App Metrics service, or a `file://` URL to write the payload to disk
|
53
|
+
# @param [String] api_token The API bearer token to use to register the metric.
|
54
|
+
# @return [Integer] the HTTP response code
|
55
|
+
#
|
56
|
+
def send_metrics(to:, api_token:, use_gzip: true)
|
57
|
+
uri = URI(to)
|
58
|
+
json_payload = use_gzip ? Zlib.gzip(to_h.to_json) : to_h.to_json
|
59
|
+
|
60
|
+
# Allow using a `file:` URI for debugging
|
61
|
+
if uri.is_a?(URI::File)
|
62
|
+
UI.message("Writing metrics payload to file #{uri.path} (instead of sending it to a remote API endpoint)")
|
63
|
+
File.write(uri.path, json_payload)
|
64
|
+
return 201 # To make it easy at call site to check for pseudo-status code 201 even in non-HTTP cases
|
65
|
+
end
|
66
|
+
|
67
|
+
UI.message("Sending metrics to #{uri}...")
|
68
|
+
headers = {
|
69
|
+
Authorization: "Bearer #{api_token}",
|
70
|
+
Accept: 'application/json',
|
71
|
+
'Content-Type': 'application/json'
|
72
|
+
}
|
73
|
+
headers[:'Content-Encoding'] = 'gzip' if use_gzip
|
74
|
+
|
75
|
+
request = Net::HTTP::Post.new(uri, headers)
|
76
|
+
request.body = json_payload
|
77
|
+
|
78
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
79
|
+
http.request(request)
|
80
|
+
end
|
81
|
+
|
82
|
+
if response.is_a?(Net::HTTPSuccess)
|
83
|
+
UI.success("Metrics sent. (#{response.code} #{response.message})")
|
84
|
+
else
|
85
|
+
UI.error("Metrics failed to send. Received: #{response.code} #{response.message}")
|
86
|
+
UI.message("Request was #{request.method} to #{request.uri}")
|
87
|
+
UI.message("Request headers were: #{headers}")
|
88
|
+
UI.message("Request body was #{request.body.length} bytes")
|
89
|
+
UI.message("Response was #{response.body}")
|
90
|
+
end
|
91
|
+
response.code.to_i
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -8,14 +8,11 @@ module Fastlane
|
|
8
8
|
# Fallback default branch of the client repository.
|
9
9
|
DEFAULT_GIT_BRANCH = 'trunk'.freeze
|
10
10
|
|
11
|
-
# Checks if the given path, or current directory if no path is given, is
|
12
|
-
# inside a Git repository
|
11
|
+
# Checks if the given path, or current directory if no path is given, is inside a Git repository
|
13
12
|
#
|
14
|
-
# @param [String] path An optional path where to check if a Git repo
|
15
|
-
# exists.
|
13
|
+
# @param [String] path An optional path where to check if a Git repo exists.
|
16
14
|
#
|
17
|
-
# @return [Bool] True if the current directory is the root of a git repo
|
18
|
-
# (i.e. a local working copy) or a subdirectory of one.
|
15
|
+
# @return [Bool] True if the current directory is the root of a git repo (i.e. a local working copy) or a subdirectory of one.
|
19
16
|
#
|
20
17
|
def self.is_git_repo?(path: Dir.pwd)
|
21
18
|
# If the path doesn't exist, find its first ancestor.
|
@@ -23,23 +20,19 @@ module Fastlane
|
|
23
20
|
# Get the path's directory, so we can look in it for the Git folder
|
24
21
|
dir = path.directory? ? path : path.dirname
|
25
22
|
|
26
|
-
# Recursively look for the Git folder until it's found or we read the
|
27
|
-
# the file system root
|
23
|
+
# Recursively look for the Git folder until it's found or we read the the file system root
|
28
24
|
dir = dir.parent until Dir.entries(dir).include?('.git') || dir.root?
|
29
25
|
|
30
|
-
# If we reached the root, we haven't found a repo.
|
31
|
-
# could be a repo in the root of the system, but that's a usecase that
|
32
|
-
# we don't need to support at this time)
|
26
|
+
# If we reached the root, we haven't found a repo.
|
27
|
+
# (Technically, there could be a repo in the root of the system, but that's a usecase that we don't need to support at this time)
|
33
28
|
return dir.root? == false
|
34
29
|
end
|
35
30
|
|
36
|
-
# Travels back the hierarchy of the given path until it finds an existing
|
37
|
-
# ancestor, or it reaches the root of the file system.
|
31
|
+
# Travels back the hierarchy of the given path until it finds an existing ancestor, or it reaches the root of the file system.
|
38
32
|
#
|
39
33
|
# @param [String] path The path to inspect
|
40
34
|
#
|
41
|
-
# @return [Pathname] The first existing ancestor, or `path` itself if it
|
42
|
-
# exists
|
35
|
+
# @return [Pathname] The first existing ancestor, or `path` itself if it exists
|
43
36
|
#
|
44
37
|
def self.first_existing_ancestor_of(path:)
|
45
38
|
p = Pathname(path).expand_path
|
@@ -106,7 +99,7 @@ module Fastlane
|
|
106
99
|
#
|
107
100
|
# @param [String] message The commit message to use
|
108
101
|
# @param [String|Array<String>] files A file or array of files to git-add before creating the commit.
|
109
|
-
#
|
102
|
+
# Use `nil` or `[]` if you already added the files in a separate step and don't wan't this method to add any new file before commit.
|
110
103
|
# Also accepts the special symbol `:all` to add all the files (`git commit -a -m …`).
|
111
104
|
# @param [Bool] push If true, will `git push` to `origin` after the commit has been created. Defaults to `false`.
|
112
105
|
#
|
@@ -209,8 +202,7 @@ module Fastlane
|
|
209
202
|
UI.user_error!("This command works only on #{branch_name} branch") unless current_branch_name.include?(branch_name)
|
210
203
|
end
|
211
204
|
|
212
|
-
# Checks whether a given path is ignored by Git, relying on Git's
|
213
|
-
# `check-ignore` under the hood.
|
205
|
+
# Checks whether a given path is ignored by Git, relying on Git's `check-ignore` under the hood.
|
214
206
|
#
|
215
207
|
# @param [String] path The path to check against `.gitignore`
|
216
208
|
#
|
@@ -143,11 +143,15 @@ module Fastlane
|
|
143
143
|
#
|
144
144
|
def self.download_glotpress_export_file(project_url:, locale:, filters:, destination:)
|
145
145
|
query_params = (filters || {}).transform_keys { |k| "filters[#{k}]" }.merge(format: 'strings')
|
146
|
-
uri = URI.parse("#{project_url.chomp('/')}/#{locale}/default/export-translations
|
146
|
+
uri = URI.parse("#{project_url.chomp('/')}/#{locale}/default/export-translations/?#{URI.encode_www_form(query_params)}")
|
147
|
+
|
148
|
+
# Set an unambiguous User Agent so GlotPress won't rate-limit us
|
149
|
+
options = { 'User-Agent' => Wpmreleasetoolkit::USER_AGENT }
|
150
|
+
|
147
151
|
begin
|
148
|
-
IO.copy_stream(uri.open, destination)
|
152
|
+
IO.copy_stream(uri.open(options), destination)
|
149
153
|
rescue StandardError => e
|
150
|
-
UI.error "Error downloading locale `#{locale}` — #{e.message}"
|
154
|
+
UI.error "Error downloading locale `#{locale}` — #{e.message} (#{uri})"
|
151
155
|
return nil
|
152
156
|
end
|
153
157
|
end
|
@@ -58,8 +58,7 @@ module Fastlane
|
|
58
58
|
#
|
59
59
|
# @param [String] input_dir The path (ideally absolute) to the directory containing the `.lproj` folders to parse
|
60
60
|
# @param [String] base_lang The code name (i.e the basename of one of the `.lproj` folders) of the locale to use as the baseline
|
61
|
-
# @return [Hash<String, String
|
62
|
-
# and the values are the output of the `diff` showing these violations.
|
61
|
+
# @return [Hash<String, Array<String>>] A hash of violations, keyed by language code, whose values are the list of violation messages for that language
|
63
62
|
#
|
64
63
|
def run(input_dir:, base_lang: DEFAULT_BASE_LANG, only_langs: nil)
|
65
64
|
check_swiftgen_installed || install_swiftgen!
|
@@ -87,7 +86,7 @@ module Fastlane
|
|
87
86
|
<<~TEMPLATE
|
88
87
|
{% macro recursiveBlock table item %}
|
89
88
|
{% for string in item.strings %}
|
90
|
-
|
89
|
+
{{string.key}} ==> [{{string.types|join:","}}]
|
91
90
|
{% endfor %}
|
92
91
|
{% for child in item.children %}
|
93
92
|
{% call recursiveBlock table child %}
|
@@ -139,20 +138,19 @@ module Fastlane
|
|
139
138
|
return [config_file, langs]
|
140
139
|
end
|
141
140
|
|
142
|
-
#
|
143
|
-
# We need to sort the output again because SwiftGen only sort case-insensitively so that means keys that are
|
144
|
-
# the same except case might be in swapped order for different outputs
|
141
|
+
# Returns a Hash mapping the list of expected parameter types for each of the keys based in the %… placeholders found in their `.strings` files
|
145
142
|
#
|
146
143
|
# @param [String] dir The temporary directory in which the file to sort lines for is located
|
147
144
|
# @param [String] lang The code for the locale we need to sort the output lines for
|
145
|
+
# @return [Hash<String, String>] A hash whose keys are the strings keys, and corresponding value is a String describing the types expected as parameters.
|
148
146
|
#
|
149
|
-
def
|
147
|
+
def placeholder_types_for_keys(dir, lang)
|
150
148
|
file = File.join(dir, output_filename(lang))
|
151
149
|
return nil unless File.exist?(file)
|
152
150
|
|
153
|
-
|
154
|
-
|
155
|
-
|
151
|
+
File.readlines(file).map do |line|
|
152
|
+
line.match(/^(.*) ==> (\[[A-Za-z,]*\])$/)&.captures
|
153
|
+
end.compact.to_h
|
156
154
|
end
|
157
155
|
|
158
156
|
# Prepares the template and config files, then run SwiftGen, run `diff` on each generated output against the baseline, and returns a Hash of the violations found.
|
@@ -160,7 +158,7 @@ module Fastlane
|
|
160
158
|
# @param [String] input_dir The directory where the `.lproj` folders to scan are located
|
161
159
|
# @param [String] base_lang The base language used as source of truth that all other languages will be compared against
|
162
160
|
# @param [Array<String>] only_langs The list of languages to limit the generation for. Useful to focus only on a couple of issues or just one language
|
163
|
-
# @return [Hash<String, String
|
161
|
+
# @return [Hash<String, Array<String>>] A hash of violations, keyed by language code, whose values are the list of violation messages for that language
|
164
162
|
#
|
165
163
|
# @note The returned Hash contains keys only for locales with violations. Locales parsed but without any violations found will not appear in the resulting hash.
|
166
164
|
#
|
@@ -172,34 +170,22 @@ module Fastlane
|
|
172
170
|
Action.sh(swiftgen_bin, 'config', 'run', '--config', config_file)
|
173
171
|
|
174
172
|
# Run diffs
|
175
|
-
|
173
|
+
params_for_base_lang = placeholder_types_for_keys(tmpdir, base_lang)
|
176
174
|
langs.delete(base_lang)
|
177
175
|
return langs.map do |lang|
|
178
|
-
|
176
|
+
params_for_lang = placeholder_types_for_keys(tmpdir, lang)
|
177
|
+
|
179
178
|
# If the lang ends up not having any translation at all (e.g. a `.lproj` without any `.strings` file in it but maybe just a storyboard or assets catalog), ignore it
|
180
|
-
next nil if
|
181
|
-
|
182
|
-
# Compute the diff
|
183
|
-
diff = `diff -U0 "#{base_file}" "#{file}"`
|
184
|
-
# Remove the lines starting with `---`/`+++` which contains the file names (which are temp files we don't want to expose in the final diff to users)
|
185
|
-
# Note: We still keep the `@@ from-file-line-numbers to-file-line-numbers @@` lines to help the user know the index of the key to find it faster,
|
186
|
-
# and also to serve as a clear visual separator between diff entries in the output.
|
187
|
-
# Those numbers won't be matching the original `.strings` file line numbers because they are line numbers in the SwiftGen-generated intermediate
|
188
|
-
# file instead, but they can still give an indication at the index in the list of keys at which this difference is located.
|
189
|
-
diff.gsub!(/^(---|\+\+\+).*\n/, '')
|
190
|
-
diff.empty? ? nil : [lang, diff]
|
191
|
-
end.compact.to_h
|
192
|
-
end
|
193
|
-
end
|
179
|
+
next nil if params_for_lang.nil? || params_for_lang.empty?
|
194
180
|
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
181
|
+
violations = params_for_lang.map do |key, param_types|
|
182
|
+
next "`#{key}` was unexpected, as it is not present in the base locale." if params_for_base_lang[key].nil?
|
183
|
+
next "`#{key}` expected placeholders for #{params_for_base_lang[key]} but found #{param_types} instead." if params_for_base_lang[key] != param_types
|
184
|
+
end.compact
|
185
|
+
|
186
|
+
[lang, violations] unless violations.empty?
|
187
|
+
end.compact.to_h
|
201
188
|
end
|
202
|
-
return true
|
203
189
|
end
|
204
190
|
end
|
205
191
|
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Fastlane
|
2
|
+
module Helper
|
3
|
+
module Ios
|
4
|
+
class StringsFileValidationHelper
|
5
|
+
# context can be one of:
|
6
|
+
# :root, :maybe_comment_start, :in_line_comment, :in_block_comment,
|
7
|
+
# :maybe_block_comment_end, :in_quoted_key,
|
8
|
+
# :after_quoted_key_before_eq, :after_quoted_key_and_equal,
|
9
|
+
# :in_quoted_value, :after_quoted_value
|
10
|
+
State = Struct.new(:context, :buffer, :in_escaped_ctx, :found_key, keyword_init: true)
|
11
|
+
|
12
|
+
TRANSITIONS = {
|
13
|
+
root: {
|
14
|
+
/\s/ => :root,
|
15
|
+
'/' => :maybe_comment_start,
|
16
|
+
'"' => :in_quoted_key
|
17
|
+
},
|
18
|
+
maybe_comment_start: {
|
19
|
+
'/' => :in_line_comment,
|
20
|
+
/\*/ => :in_block_comment
|
21
|
+
},
|
22
|
+
in_line_comment: {
|
23
|
+
"\n" => :root,
|
24
|
+
/./ => :in_line_comment
|
25
|
+
},
|
26
|
+
in_block_comment: {
|
27
|
+
/\*/ => :maybe_block_comment_end,
|
28
|
+
/./m => :in_block_comment
|
29
|
+
},
|
30
|
+
maybe_block_comment_end: {
|
31
|
+
'/' => :root,
|
32
|
+
/./m => :in_block_comment
|
33
|
+
},
|
34
|
+
in_quoted_key: {
|
35
|
+
'"' => lambda do |state, _|
|
36
|
+
state.found_key = state.buffer.string.dup
|
37
|
+
state.buffer.string = ''
|
38
|
+
:after_quoted_key_before_eq
|
39
|
+
end,
|
40
|
+
/./ => lambda do |state, c|
|
41
|
+
state.buffer.write(c)
|
42
|
+
:in_quoted_key
|
43
|
+
end
|
44
|
+
},
|
45
|
+
after_quoted_key_before_eq: {
|
46
|
+
/\s/ => :after_quoted_key_before_eq,
|
47
|
+
'=' => :after_quoted_key_and_eq
|
48
|
+
},
|
49
|
+
after_quoted_key_and_eq: {
|
50
|
+
/\s/ => :after_quoted_key_and_eq,
|
51
|
+
'"' => :in_quoted_value
|
52
|
+
},
|
53
|
+
in_quoted_value: {
|
54
|
+
'"' => :after_quoted_value,
|
55
|
+
/./m => :in_quoted_value
|
56
|
+
},
|
57
|
+
after_quoted_value: {
|
58
|
+
/\s/ => :after_quoted_value,
|
59
|
+
';' => :root
|
60
|
+
}
|
61
|
+
}.freeze
|
62
|
+
|
63
|
+
# Inspects the given `.strings` file for duplicated keys, returning them if any.
|
64
|
+
#
|
65
|
+
# @param [String] file The path to the file to inspect.
|
66
|
+
# @return [Hash<String, Array<Int>] Hash with the dublipcated keys.
|
67
|
+
# Each element has the duplicated key (from the `.strings`) as key and an array of line numbers where the key occurs as value.
|
68
|
+
def self.find_duplicated_keys(file:)
|
69
|
+
keys_with_lines = Hash.new([])
|
70
|
+
|
71
|
+
state = State.new(context: :root, buffer: StringIO.new, in_escaped_ctx: false, found_key: nil)
|
72
|
+
|
73
|
+
File.readlines(file).each_with_index do |line, line_no|
|
74
|
+
line.chars.each_with_index do |c, col_no|
|
75
|
+
# Handle escaped characters at a global level.
|
76
|
+
# This is more straightforward than having to account for it in the `TRANSITIONS` table.
|
77
|
+
if state.in_escaped_ctx || c == '\\'
|
78
|
+
# Just because we check for escaped characters at the global level, it doesn't mean we allow them in every context.
|
79
|
+
allowed_contexts_for_escaped_characters = %i[in_quoted_key in_quoted_value in_block_comment in_line_comment]
|
80
|
+
raise "Found escaped character outside of allowed contexts on line #{line_no + 1} (current context: #{state.context})" unless allowed_contexts_for_escaped_characters.include?(state.context)
|
81
|
+
|
82
|
+
state.buffer.write(c) if state.context == :in_quoted_key
|
83
|
+
state.in_escaped_ctx = !state.in_escaped_ctx
|
84
|
+
next
|
85
|
+
end
|
86
|
+
|
87
|
+
# Look at the transitions table for the current context, and find the first transition matching the current character
|
88
|
+
(_, next_context) = TRANSITIONS[state.context].find { |regex, _| c.match?(regex) } || [nil, nil]
|
89
|
+
raise "Invalid character `#{c}` found on line #{line_no + 1}, col #{col_no + 1}" if next_context.nil?
|
90
|
+
|
91
|
+
state.context = next_context.is_a?(Proc) ? next_context.call(state, c) : next_context
|
92
|
+
next unless state.found_key
|
93
|
+
|
94
|
+
# If we just exited the :in_quoted_key context and thus have found a new key, process it
|
95
|
+
key = state.found_key.dup
|
96
|
+
state.found_key = nil
|
97
|
+
|
98
|
+
keys_with_lines[key] += [line_no + 1]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
keys_with_lines.keep_if { |_, lines| lines.count > 1 }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -12,8 +12,7 @@ module Fastlane
|
|
12
12
|
@alternates = {}
|
13
13
|
end
|
14
14
|
|
15
|
-
# Downloads data from GlotPress,
|
16
|
-
# in JSON format
|
15
|
+
# Downloads data from GlotPress, in JSON format
|
17
16
|
def download(target_locale, glotpress_url, is_source)
|
18
17
|
uri = URI(glotpress_url)
|
19
18
|
response = Net::HTTP.get_response(uri)
|
@@ -73,15 +72,14 @@ module Fastlane
|
|
73
72
|
UI.message("#{target_locale} translation for #{key} exceeds maximum length (#{message_len}). Switching to the alternate translation.")
|
74
73
|
@alternates[data[:alternate_key]] = { desc: data[:desc], max_size: 0 }
|
75
74
|
else
|
76
|
-
UI.message("Rejecting #{target_locale}
|
75
|
+
UI.message("Rejecting #{target_locale} translation for #{key}: translation length: #{message_len} - max allowed length: #{data[:max_size]}")
|
77
76
|
end
|
78
77
|
else
|
79
78
|
save_metadata(target_locale, file[1][:desc], msg)
|
80
79
|
end
|
81
80
|
end
|
82
81
|
|
83
|
-
# Writes the downloaded content
|
84
|
-
# to the target file
|
82
|
+
# Writes the downloaded content to the target file
|
85
83
|
def save_metadata(locale, file_name, content)
|
86
84
|
file_path = get_target_file_path(locale, file_name)
|
87
85
|
|
@@ -117,8 +117,7 @@ module Fastlane
|
|
117
117
|
end
|
118
118
|
|
119
119
|
def draw_screenshot_to_canvas(entry, canvas, device)
|
120
|
-
# Don't require a screenshot to be present – we can just skip
|
121
|
-
# this function if one doesn't exist.
|
120
|
+
# Don't require a screenshot to be present – we can just skip this function if one doesn't exist.
|
122
121
|
return canvas if entry['screenshot'].nil?
|
123
122
|
|
124
123
|
device_mask = device['screenshot_mask']
|
@@ -235,7 +234,7 @@ module Fastlane
|
|
235
234
|
begin
|
236
235
|
tempTextFile = Tempfile.new()
|
237
236
|
|
238
|
-
sh('drawText', "html
|
237
|
+
Action.sh('drawText', "html=#{text}", "maxWidth=#{width}", "maxHeight=#{height}", "output=#{tempTextFile.path}", "fontSize=#{font_size}", "stylesheet=#{stylesheet_path}", "alignment=#{position}")
|
239
238
|
|
240
239
|
text_content = open_image(tempTextFile.path).trim
|
241
240
|
text_frame = create_image(width, height)
|
@@ -9,9 +9,11 @@ module Fastlane
|
|
9
9
|
def self.add_new_section(path:, section_title:)
|
10
10
|
lines = File.readlines(path)
|
11
11
|
|
12
|
-
# Find the index of the first non-empty line that is also NOT a comment.
|
12
|
+
# Find the index of the first non-empty line that is also NOT a comment.
|
13
|
+
# That way we keep commment headers as the very top of the file
|
13
14
|
line_idx = lines.find_index { |l| !l.start_with?('***') && !l.start_with?('//') && !l.chomp.empty? }
|
14
|
-
# Put back the header, then the new entry, then the rest
|
15
|
+
# Put back the header, then the new entry, then the rest
|
16
|
+
# (note: '...' excludes the higher bound of the range, unlike '..')
|
15
17
|
new_lines = lines[0...line_idx] + ["#{section_title}\n", "-----\n", "\n", "\n"] + lines[line_idx..]
|
16
18
|
|
17
19
|
File.write(path, new_lines.join)
|
@@ -40,11 +40,9 @@ module Fastlane
|
|
40
40
|
end
|
41
41
|
|
42
42
|
# "Applies" the instruction described in the instance to the file system.
|
43
|
-
# That is, copies the content of the source `file` to the `destination`
|
44
|
-
# path.
|
43
|
+
# That is, copies the content of the source `file` to the `destination` path.
|
45
44
|
#
|
46
|
-
# @raise [StandardError] For security reasons, it will raise if
|
47
|
-
# `destination` is not ignored under Git.
|
45
|
+
# @raise [StandardError] For security reasons, it will raise if `destination` is not ignored under Git.
|
48
46
|
def apply
|
49
47
|
# Only decrypt the file if the destination is ignored in Git
|
50
48
|
unless Fastlane::Helper::GitHelper.is_ignored?(path: destination_file_path)
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Fastlane
|
2
|
+
class FirebaseAccount
|
3
|
+
def self.activate_service_account_with_key_file(key_file_path)
|
4
|
+
Fastlane::Actions.sh('gcloud', 'auth', 'activate-service-account', '--key-file', key_file_path)
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.authenticated?
|
8
|
+
auth_status = JSON.parse(auth_status_data)
|
9
|
+
auth_status.any? do |account|
|
10
|
+
account['status'] == 'ACTIVE'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Lookup the current account authentication status
|
15
|
+
def self.auth_status_data
|
16
|
+
Fastlane::Actions.sh('gcloud', 'auth', 'list', '--format', 'json', log: false)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Fastlane
|
2
|
+
class FirebaseDevice
|
3
|
+
attr_reader :model, :version, :locale, :orientation
|
4
|
+
|
5
|
+
def initialize(model:, version:, orientation:, locale: 'en')
|
6
|
+
raise 'Invalid Model' unless FirebaseDevice.valid_model_names.include? model
|
7
|
+
raise 'Invalid Version' unless FirebaseDevice.valid_version_numbers.include? version
|
8
|
+
raise 'Invalid Locale' unless FirebaseDevice.valid_locales.include? locale
|
9
|
+
raise 'Invalid Orientation' unless FirebaseDevice.valid_orientations.include? orientation
|
10
|
+
|
11
|
+
@model = model
|
12
|
+
@version = version
|
13
|
+
@locale = locale
|
14
|
+
@orientation = orientation
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
"model=#{@model},version=#{@version},locale=#{@locale},orientation=#{@orientation}"
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
@locale_data = nil
|
23
|
+
@model_data = nil
|
24
|
+
@version_data = nil
|
25
|
+
|
26
|
+
def valid_model_names
|
27
|
+
JSON.parse(model_data).map { |device| device['codename'] }
|
28
|
+
end
|
29
|
+
|
30
|
+
def valid_version_numbers
|
31
|
+
JSON.parse(version_data).map { |version| version['apiLevel'].to_i }
|
32
|
+
end
|
33
|
+
|
34
|
+
def valid_locales
|
35
|
+
JSON.parse(locale_data).map { |locale| locale['id'] }
|
36
|
+
end
|
37
|
+
|
38
|
+
def valid_orientations
|
39
|
+
%w[portrait landscape]
|
40
|
+
end
|
41
|
+
|
42
|
+
def locale_data
|
43
|
+
FirebaseDevice.verify_logged_in!
|
44
|
+
@locale_data ||= Fastlane::Actions.sh('gcloud', 'firebase', 'test', 'android', 'locales', 'list', '--format="json"', log: false)
|
45
|
+
end
|
46
|
+
|
47
|
+
def model_data
|
48
|
+
FirebaseDevice.verify_logged_in!
|
49
|
+
@model_data ||= Fastlane::Actions.sh('gcloud', 'firebase', 'test', 'android', 'models', 'list', '--format="json"', log: false)
|
50
|
+
end
|
51
|
+
|
52
|
+
def version_data
|
53
|
+
FirebaseDevice.verify_logged_in!
|
54
|
+
@version_data ||= Fastlane::Actions.sh('gcloud', 'firebase', 'test', 'android', 'versions', 'list', '--format="json"', log: false)
|
55
|
+
end
|
56
|
+
|
57
|
+
def verify_logged_in!
|
58
|
+
UI.user_error!('You must call `firebase_login` before creating a FirebaseDevice object') unless FirebaseAccount.authenticated?
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Fastlane
|
2
|
+
class FirebaseTestLabResult
|
3
|
+
def initialize(log_file_path:)
|
4
|
+
raise "No log file found at path #{log_file_path}" unless File.file? log_file_path
|
5
|
+
|
6
|
+
@path = log_file_path
|
7
|
+
end
|
8
|
+
|
9
|
+
# Scan the log file to for indications that no test cases failed
|
10
|
+
def success?
|
11
|
+
File.readlines(@path).any? { |line| line.include?('Passed') && line.include?('test cases passed') }
|
12
|
+
end
|
13
|
+
|
14
|
+
# Parse the log for the "More details are available..." URL
|
15
|
+
def more_details_url
|
16
|
+
File.readlines(@path)
|
17
|
+
.flat_map { |line| URI.extract(line) }
|
18
|
+
.find { |url| URI(url).host == 'console.firebase.google.com' && url.include?('/matrices/') }
|
19
|
+
end
|
20
|
+
|
21
|
+
# Parse the log for the Google Cloud Storage Bucket URL
|
22
|
+
def raw_results_paths
|
23
|
+
uri = File.readlines(@path)
|
24
|
+
.flat_map { |line| URI.extract(line) }
|
25
|
+
.map { |string| URI(string) }
|
26
|
+
.find { |u| u.scheme == 'gs' }
|
27
|
+
|
28
|
+
return nil if uri.nil?
|
29
|
+
|
30
|
+
return {
|
31
|
+
bucket: uri.host,
|
32
|
+
prefix: uri.path.delete_prefix('/').chomp('/')
|
33
|
+
}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|