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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_localize_libs_action.rb +10 -5
  3. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_update_metadata_source_action.rb +1 -1
  4. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/an_validate_lib_strings_action.rb +1 -1
  5. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_betabuild_prechecks.rb +1 -1
  6. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_build_prechecks.rb +1 -1
  7. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_build_preflight.rb +1 -1
  8. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_beta.rb +1 -1
  9. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_final_release.rb +1 -1
  10. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_hotfix.rb +1 -1
  11. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_release.rb +1 -1
  12. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_codefreeze_prechecks.rb +1 -1
  13. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_completecodefreeze_prechecks.rb +1 -1
  14. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_create_xml_release_notes.rb +1 -1
  15. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_current_branch_is_hotfix.rb +1 -1
  16. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_download_file_by_version.rb +1 -1
  17. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_download_translations_action.rb +1 -1
  18. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_finalize_prechecks.rb +1 -1
  19. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_firebase_test.rb +187 -0
  20. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_alpha_version.rb +1 -1
  21. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_app_version.rb +1 -1
  22. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_release_version.rb +1 -1
  23. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/{android_hotifx_prechecks.rb → android_hotfix_prechecks.rb} +1 -1
  24. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb +279 -0
  25. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_tag_build.rb +1 -1
  26. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_update_release_notes.rb +1 -1
  27. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/check_for_toolkit_updates_action.rb +1 -1
  28. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/check_translation_progress.rb +1 -1
  29. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/circleci_trigger_job_action.rb +1 -1
  30. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/close_milestone_action.rb +1 -1
  31. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_new_milestone_action.rb +1 -1
  32. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_release_action.rb +1 -1
  33. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/extract_release_notes_for_version_action.rb +1 -1
  34. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/firebase_login.rb +44 -0
  35. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/get_prs_list_action.rb +1 -1
  36. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/gp_downloadmetadata_action.rb +1 -1
  37. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/gp_update_metadata_source.rb +1 -2
  38. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/promo_screenshots_action.rb +1 -1
  39. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/removebranchprotection_action.rb +1 -1
  40. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/setbranchprotection_action.rb +1 -1
  41. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/setfrozentag_action.rb +1 -1
  42. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_add_files_to_copy_action.rb +1 -1
  43. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_apply_action.rb +1 -1
  44. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_download_action.rb +2 -3
  45. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_setup_action.rb +1 -1
  46. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_update_action.rb +1 -1
  47. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/configure/configure_validate_action.rb +3 -3
  48. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/add_development_certificates_to_provisioning_profiles.rb +1 -1
  49. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/add_devices_to_provisioning_profiles.rb +1 -1
  50. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_betabuild_prechecks.rb +1 -1
  51. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_build_prechecks.rb +1 -1
  52. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_build_preflight.rb +1 -1
  53. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_bump_version_beta.rb +1 -1
  54. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_bump_version_hotfix.rb +1 -1
  55. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_bump_version_release.rb +1 -1
  56. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_check_beta_deps.rb +1 -1
  57. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_clear_intermediate_tags.rb +1 -1
  58. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_codefreeze_prechecks.rb +1 -1
  59. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_completecodefreeze_prechecks.rb +1 -1
  60. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_current_branch_is_hotfix.rb +1 -1
  61. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_final_tag.rb +1 -1
  62. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_finalize_prechecks.rb +1 -1
  63. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_app_version.rb +1 -1
  64. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_build_version.rb +1 -1
  65. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_get_store_app_sizes.rb +1 -1
  66. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/{ios_hotifx_prechecks.rb → ios_hotfix_prechecks.rb} +1 -1
  67. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_lint_localizations.rb +48 -9
  68. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_localize_project.rb +1 -1
  69. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_merge_strings_files.rb +1 -1
  70. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb +170 -0
  71. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_tag_build.rb +1 -1
  72. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_metadata.rb +1 -1
  73. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_metadata_source.rb +1 -1
  74. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_release_notes.rb +1 -1
  75. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_validate_ci_build.rb +1 -1
  76. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb +72 -49
  77. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb +95 -0
  78. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/git_helper.rb +10 -18
  79. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb +7 -3
  80. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_linter_helper.rb +20 -34
  81. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_strings_file_validation_helper.rb +107 -0
  82. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/metadata_download_helper.rb +3 -5
  83. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/promo_screenshots_helper.rb +2 -3
  84. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/release_notes_helper.rb +4 -2
  85. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/user_agent.rb +5 -0
  86. data/lib/fastlane/plugin/wpmreleasetoolkit/models/file_reference.rb +2 -4
  87. data/lib/fastlane/plugin/wpmreleasetoolkit/models/firebase_account.rb +19 -0
  88. data/lib/fastlane/plugin/wpmreleasetoolkit/models/firebase_device.rb +62 -0
  89. data/lib/fastlane/plugin/wpmreleasetoolkit/models/firebase_test_lab_result.rb +36 -0
  90. data/lib/fastlane/plugin/wpmreleasetoolkit/models/firebase_test_runner.rb +104 -0
  91. data/lib/fastlane/plugin/wpmreleasetoolkit/version.rb +1 -1
  92. data/lib/fastlane/plugin/wpmreleasetoolkit.rb +2 -2
  93. 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. (Technically, there
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
- # 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.
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?#{URI.encode_www_form(query_params)}")
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>] A hash whose keys are the language codes (basename of `.lproj` folders) for which violations were found,
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
- "{{string.key}}" => [{{string.types|join:","}}]
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
- # Because we use English copy verbatim as key names, some keys are the same except for the upper/lowercase.
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 sort_file_lines!(dir, lang)
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
- sorted_lines = File.readlines(file).sort
154
- File.write(file, sorted_lines.join)
155
- return file
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>] A hash of violations, keyed by language code, whose values are the diff output.
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
- base_file = sort_file_lines!(tmpdir, base_lang)
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
- file = sort_file_lines!(tmpdir, lang)
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 file.nil? || only_empty_lines?(file)
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
- # Returns true if the file only contains empty lines, i.e. lines that only contains whitespace (space, tab, CR, LF)
196
- def only_empty_lines?(file)
197
- File.open(file) do |f|
198
- while (line = f.gets)
199
- return false unless line.strip.empty?
200
- end
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} traslation for #{key}: translation length: #{message_len} - max allowed length: #{data[:max_size]}")
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=\"#{text}\"", "maxWidth=#{width}", "maxHeight=#{height}", "output=\"#{tempTextFile.path}\"", "fontSize=#{font_size}", "stylesheet=\"#{stylesheet_path}\"", "alignment=\"#{position}\"")
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. That way we keep commment headers as the very top of the file
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 (note: '...' excludes the higher bound of the range, unlike '..')
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)
@@ -0,0 +1,5 @@
1
+ module Fastlane
2
+ module Wpmreleasetoolkit
3
+ USER_AGENT = 'Automattic App Release Automator; https://github.com/wordpress-mobile/release-toolkit/'.freeze
4
+ end
5
+ end
@@ -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