fastlane-plugin-wpmreleasetoolkit 11.0.2 → 11.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a01dbbf909c6d6ae5a683e4d4261e44ef7dda1f9b896e0f49529de45ffb8c58
4
- data.tar.gz: 5b6343174100d8167d85b7a31fe2484988877eb249d4ee527469fc025d201a26
3
+ metadata.gz: 1f9d86b44058a8ff2f28f2c56bdb81230395e36eac41f6e4d0761140f9694177
4
+ data.tar.gz: cc234c6029919a63d4d9ed2114d390bb58ad79e67f7ae1650e94b23cfac92bc9
5
5
  SHA512:
6
- metadata.gz: 10035929825ca4d666f3a75d184feba49720233b0a57a0e00ac7f9cf92097f332f2554a3e9f811ebdb5dda83a9d6b750dc22568bf3cddd992509f12ca551ee2b
7
- data.tar.gz: 0a67262b02fbf58244a5c779a6e517a27aa869c2e872c10f298a4c2621837e46206a78e4ec4c292df5f2086eb02054ea86470062435c7683f44a1a97bf278aec
6
+ metadata.gz: e2e86d40c624300445ddc600e19b8b16a560e86725c82344719bbbadb69ad0c1552fff4f951cdb5411d892973fa37b524b47bcc99b0d14a23bff56607d44f676
7
+ data.tar.gz: faf08e93483cced02d83d705b9cff2843e07244c708312bdf8dfb86131b2990d4e5ba94397aa71aadd8a3cb6d485da2007b5743950e15486ff81243f8a956b35
@@ -0,0 +1,215 @@
1
+ require 'fastlane/action'
2
+ require_relative '../../helper/github_helper'
3
+
4
+ module Fastlane
5
+ module Actions
6
+ class CreateReleaseBackmergePullRequestAction < Action
7
+ DEFAULT_BRANCH = 'trunk'.freeze
8
+
9
+ def self.run(params)
10
+ token = params[:github_token]
11
+ repository = params[:repository]
12
+ source_branch = params[:source_branch]
13
+ default_branch = params[:default_branch]
14
+ target_branches = params[:target_branches]
15
+ labels = params[:labels]
16
+ milestone_title = params[:milestone_title]
17
+ reviewers = params[:reviewers]
18
+ team_reviewers = params[:team_reviewers]
19
+ intermediate_branch_created_callback = params[:intermediate_branch_created_callback]
20
+
21
+ if target_branches.include?(source_branch)
22
+ UI.user_error!('`target_branches` must not contain `source_branch`')
23
+ end
24
+
25
+ github_helper = Fastlane::Helper::GithubHelper.new(github_token: params[:github_token])
26
+ target_milestone = milestone_title.nil? ? nil : github_helper.get_milestone(repository, milestone_title)
27
+
28
+ final_target_branches = if target_branches.empty?
29
+ unless source_branch.start_with?('release/')
30
+ UI.user_error!('`source_branch` must start with `release/`')
31
+ end
32
+
33
+ determine_target_branches(source_release_version: source_branch.delete('release/'), default_branch: default_branch)
34
+ else
35
+ target_branches
36
+ end
37
+
38
+ final_target_branches.map do |target_branch|
39
+ Fastlane::Helper::GitHelper.checkout_and_pull(source_branch)
40
+
41
+ create_backmerge_pr(
42
+ token: token,
43
+ repository: repository,
44
+ title: "Merge #{source_branch} into #{target_branch}",
45
+ head_branch: source_branch,
46
+ base_branch: target_branch,
47
+ labels: labels,
48
+ milestone: target_milestone&.number,
49
+ reviewers: reviewers,
50
+ team_reviewers: team_reviewers,
51
+ intermediate_branch_created_callback: intermediate_branch_created_callback
52
+ )
53
+ end.compact
54
+ end
55
+
56
+ # Determines the target branches for a release version.
57
+ #
58
+ # @param source_release_version [String] the source release version to compare against other release branches.
59
+ # @param default_branch [String] the default branch to use if no target branches are found.
60
+ # @return [Array<String>] the list of target branches greater than the release version.
61
+ def self.determine_target_branches(source_release_version:, default_branch:)
62
+ release_branches = Actions.sh('git', 'branch', '-r', '-l', 'origin/release/*').strip.split("\n")
63
+
64
+ all_release_branches_versions = release_branches
65
+ .map { |branch| branch.match(%r{origin/release/([0-9.]*)})&.captures&.first }
66
+ .compact
67
+
68
+ target_branches = all_release_branches_versions.select { |branch| Gem::Version.new(branch) > Gem::Version.new(source_release_version) }
69
+ .map { |v| "release/#{v}" }
70
+ target_branches = [default_branch] if target_branches.empty?
71
+
72
+ target_branches
73
+ end
74
+
75
+ # Creates a backmerge pull request using the `create_pull_request` Fastlane Action.
76
+ #
77
+ # @param token [String] the GitHub token for authentication.
78
+ # @param repository [String] the repository where the pull request will be created.
79
+ # @param title [String] the title of the pull request.
80
+ # @param head_branch [String] the source branch for the pull request.
81
+ # @param base_branch [String] the target branch for the pull request.
82
+ # @param labels [Array<String>] the labels to add to the pull request.
83
+ # @param milestone [String] the milestone to associate with the pull request.
84
+ # @param reviewers [Array<String>] the individual reviewers for the pull request.
85
+ # @param team_reviewers [Array<String>] the team reviewers for the pull request.
86
+ # @param intermediate_branch_created_callback [Proc] A callback to call after having created the intermediate branch
87
+ # to allow the caller to e.g. add new commits on it before the PR is created. The callback takes two parameters: the base branch and the intermediate branch
88
+ #
89
+ # @return [String] The URL of the created Pull Request, or `nil` if no PR was created.
90
+ #
91
+ def self.create_backmerge_pr(token:, repository:, title:, head_branch:, base_branch:, labels:, milestone:, reviewers:, team_reviewers:, intermediate_branch_created_callback:)
92
+ intermediate_branch = "merge/#{head_branch.gsub('/', '-')}-into-#{base_branch.gsub('/', '-')}"
93
+
94
+ if Fastlane::Helper::GitHelper.branch_exists_on_remote?(branch_name: intermediate_branch)
95
+ UI.user_error!("The intermediate branch `#{intermediate_branch}` already exists. Please check if there is an existing Pull Request that needs to be merged or closed first, or delete the branch.")
96
+ return nil
97
+ end
98
+
99
+ Fastlane::Helper::GitHelper.create_branch(intermediate_branch)
100
+
101
+ intermediate_branch_created_callback&.call(base_branch, intermediate_branch)
102
+
103
+ # if there's a callback, make sure it didn't switch branches
104
+ other_action.ensure_git_branch(branch: "^#{intermediate_branch}/") unless intermediate_branch_created_callback.nil?
105
+
106
+ if Fastlane::Helper::GitHelper.point_to_same_commit?(base_branch, head_branch)
107
+ UI.error("No differences between #{head_branch} and #{base_branch}. Skipping PR creation.")
108
+ return nil
109
+ end
110
+
111
+ other_action.push_to_git_remote(tags: false)
112
+
113
+ pr_body = <<~BODY
114
+ Merging `#{head_branch}` into `#{base_branch}`.
115
+
116
+ Via intermediate branch `#{intermediate_branch}`, to help fix conflicts if any:
117
+ ```
118
+ #{head_branch.rjust(40)} ----o-- - - -
119
+ #{' ' * 40} \\
120
+ #{intermediate_branch.rjust(40)} `---.
121
+ #{' ' * 40} \\
122
+ #{base_branch.rjust(40)} ------------x- - -
123
+ ```
124
+ BODY
125
+
126
+ other_action.create_pull_request(
127
+ api_token: token,
128
+ repo: repository,
129
+ title: title,
130
+ body: pr_body,
131
+ head: intermediate_branch,
132
+ base: base_branch,
133
+ labels: labels,
134
+ milestone: milestone,
135
+ reviewers: reviewers,
136
+ team_reviewers: team_reviewers
137
+ )
138
+ end
139
+
140
+ def self.description
141
+ 'Creates backmerge PRs for a release branch into target branches'
142
+ end
143
+
144
+ def self.authors
145
+ ['Automattic']
146
+ end
147
+
148
+ def self.return_type
149
+ :array_of_strings
150
+ end
151
+
152
+ def self.return_value
153
+ 'The list of the created backmerge Pull Request URLs'
154
+ end
155
+
156
+ def self.details
157
+ <<~DETAILS
158
+ This action creates backmerge Pull Requests from a release branch into one or more target branches.
159
+
160
+ It can be used to ensure that changes from a release branch are merged back into other branches, such as newer release branches or the main development branch (e.g., `trunk`).
161
+ DETAILS
162
+ end
163
+
164
+ def self.available_options
165
+ [
166
+ FastlaneCore::ConfigItem.new(key: :repository,
167
+ env_name: 'GHHELPER_REPOSITORY',
168
+ description: 'The remote path of the GH repository on which we work',
169
+ optional: false,
170
+ type: String),
171
+ FastlaneCore::ConfigItem.new(key: :source_branch,
172
+ description: 'The source branch to create a backmerge PR from, in the format `release/x.y.z`',
173
+ optional: false,
174
+ type: String),
175
+ FastlaneCore::ConfigItem.new(key: :default_branch,
176
+ description: 'The default branch to target if no newer release branches exist',
177
+ optional: true,
178
+ default_value: DEFAULT_BRANCH,
179
+ type: String),
180
+ FastlaneCore::ConfigItem.new(key: :target_branches,
181
+ description: 'Array of target branches for the backmerge. If empty, the action will determine target branches by finding all `release/x.y.z` branches with a `x.y.z` version greater than the version in source branch\'s name. If none are found, it will target `default_branch`', # rubocop:disable Layout/LineLength
182
+ optional: true,
183
+ default_value: [],
184
+ type: Array),
185
+ FastlaneCore::ConfigItem.new(key: :labels,
186
+ description: 'The labels that should be assigned to the backmerge PRs',
187
+ optional: true,
188
+ default_value: [],
189
+ type: Array),
190
+ FastlaneCore::ConfigItem.new(key: :milestone_title,
191
+ description: 'The title of the milestone to assign to the created PRs',
192
+ optional: true,
193
+ type: String),
194
+ FastlaneCore::ConfigItem.new(key: :reviewers,
195
+ description: 'An array of GitHub users that will be assigned to the pull request',
196
+ optional: true,
197
+ type: Array),
198
+ FastlaneCore::ConfigItem.new(key: :team_reviewers,
199
+ description: 'An array of GitHub team slugs that will be assigned to the pull request',
200
+ optional: true,
201
+ type: Array),
202
+ FastlaneCore::ConfigItem.new(key: :intermediate_branch_created_callback,
203
+ description: 'Callback to allow for the caller to perform operations on the intermediate branch before pushing. The call back receives two parameters: the base (target) branch for the PR and the intermediate branch name',
204
+ optional: true,
205
+ type: Proc),
206
+ Fastlane::Helper::GithubHelper.github_token_config_item,
207
+ ]
208
+ end
209
+
210
+ def self.is_supported?(platform)
211
+ true
212
+ end
213
+ end
214
+ end
215
+ end
@@ -152,6 +152,13 @@ module Fastlane
152
152
  (added_count + updated_count) != 0
153
153
  end
154
154
 
155
+ ########
156
+ # @!group Verify diff of library vs main strings matches
157
+ #
158
+ # @note This set of methods is used by `an_validate_lib_strings_action`
159
+ # (which doesn't seem to be used by any of our Android projects nowadays?)
160
+ ########
161
+
155
162
  def self.verify_diff(diff_string, main_strings, lib_strings, library)
156
163
  return unless diff_string.start_with?('name=')
157
164
 
@@ -198,6 +205,8 @@ module Fastlane
198
205
  end
199
206
  end
200
207
 
208
+ # @!endgroup
209
+
201
210
  ########
202
211
  # @!group Downloading translations from GlotPress
203
212
  ########
@@ -223,7 +232,7 @@ module Fastlane
223
232
  #
224
233
  # @param [String] res_dir The relative path to the `…/src/main/res` directory.
225
234
  # @param [String] glotpress_project_url The base URL to the glotpress project to download the strings from.
226
- # @param [Hash{String=>String}, Array] glotpress_filters
235
+ # @param [Hash{Symbol=>String}, Array] glotpress_filters
227
236
  # The filters to apply when exporting strings from GlotPress.
228
237
  # Typical examples include `{ status: 'current' }` or `{ status: 'review' }`.
229
238
  # If an array of Hashes is provided instead of a single Hash, this method will perform as many
@@ -235,10 +244,8 @@ module Fastlane
235
244
  def self.download_from_glotpress(res_dir:, glotpress_project_url:, locales_map:, glotpress_filters: { status: 'current' })
236
245
  glotpress_filters = [glotpress_filters] unless glotpress_filters.is_a?(Array)
237
246
 
238
- attributes_to_copy = %w[formatted] # Attributes that we want to replicate into translated `string.xml` files
239
247
  orig_file = File.join(res_dir, 'values', 'strings.xml')
240
248
  orig_xml = File.open(orig_file) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }
241
- orig_attributes = orig_xml.xpath('//string').to_h { |tag| [tag['name'], tag.attributes.select { |k, _| attributes_to_copy.include?(k) }] }
242
249
 
243
250
  locales_map.each do |lang_codes|
244
251
  all_xml_documents = glotpress_filters.map do |filters|
@@ -250,13 +257,7 @@ module Fastlane
250
257
  # Merge all XMLs together
251
258
  merged_xml = merge_xml_documents(all_xml_documents)
252
259
 
253
- # Process XML (text substitutions, replicate attributes, quick-lint string)
254
- merged_xml.xpath('//string').each do |string_tag|
255
- apply_substitutions(string_tag)
256
- orig_attributes[string_tag['name']]&.each { |k, v| string_tag[k] = v }
257
- quick_lint(string_tag, lang_codes[:android])
258
- end
259
- merged_xml.xpath('//string-array/item').each { |item_tag| apply_substitutions(item_tag) }
260
+ post_process_xml!(merged_xml, locale_code: lang_codes[:android], original_xml: orig_xml)
260
261
 
261
262
  # Save
262
263
  lang_dir = File.join(res_dir, "values-#{lang_codes[:android]}")
@@ -266,8 +267,10 @@ module Fastlane
266
267
  end
267
268
  end
268
269
 
270
+ # @!endgroup
271
+
269
272
  #####################
270
- # Private Helpers
273
+ # @!group Private Helpers
271
274
  #####################
272
275
 
273
276
  # Downloads the export from GlotPress for a given locale and given filters
@@ -276,7 +279,7 @@ module Fastlane
276
279
  # @param [String] locale The GlotPress locale code to download strings for.
277
280
  # @param [Hash{Symbol=>String}] filters The hash of filters to apply when exporting from GlotPress.
278
281
  # Typical examples include `{ status: 'current' }` or `{ status: 'review' }`.
279
- # @return [Nokogiri::XML] the download XML document, parsed as a Nokogiri::XML object
282
+ # @return [Nokogiri::XML::Document] the download XML document, parsed as a Nokogiri::XML object
280
283
  #
281
284
  def self.download_glotpress_export_file(project_url:, locale:, filters:)
282
285
  query_params = filters.transform_keys { |k| "filters[#{k}]" }.merge(format: 'android')
@@ -297,6 +300,9 @@ module Fastlane
297
300
 
298
301
  # Merge multiple Nokogiri::XML `strings.xml` documents together
299
302
  #
303
+ # Used especially when we provided multiple GlotPress filters to `download_from_glotpress`,
304
+ # as in this case we'd trigger one export per filter, then merge the result in a single XML
305
+ #
300
306
  # @param [Array<Nokogiri::XML::Document>] all_xmls Array of the Nokogiri XML documents to merge together
301
307
  # @return [Nokogiri::XML::Document] The merged document.
302
308
  #
@@ -323,11 +329,53 @@ module Fastlane
323
329
  end
324
330
  private_class_method :merge_xml_documents
325
331
 
326
- # Apply some common text substitutions to tag contents
332
+ # Process a downloaded XML (in-place), to apply the following
333
+ # - replicate attributes from the nodes of the original XML (`translatable`, `tools:ignore`, …) to the translated XML
334
+ # - text substitutions for common special characters
335
+ # - quick-lint string by searching for common issue patterns (using `%%` in a `formatted=false` string, etc)
336
+ #
337
+ # @param [Nokogiri::XML::Document] translated_xml The downloaded XML to post-process
338
+ # @param [String] locale_code The android locale code associated with the translated_xml
339
+ # @param [Nokogiri::XML::Document] original_xml The original `values/strings.xml` to use as reference
340
+ #
341
+ def self.post_process_xml!(translated_xml, locale_code:, original_xml:)
342
+ copy_orig_attributes = lambda do |node, xpath|
343
+ orig_attributes = original_xml.xpath(xpath)&.first&.attribute_nodes&.to_h do |attr|
344
+ [[attr.namespace&.prefix, attr.name].compact.join(':'), attr.value]
345
+ end
346
+ orig_attributes&.each { |k, v| node[k] = v unless k == 'name' }
347
+ end
348
+
349
+ # 1. Replicate namespaces on the document (especially `xmlns:tools` if present)
350
+ original_xml.namespaces.each { |k, v| translated_xml.root&.add_namespace(k.delete_prefix('xmlns:'), v) }
351
+ # 2. Replicate attributes on any node with `@name` attribute (`string`, `string-array`, `plurals`)
352
+ translated_xml.xpath('//*[@name]').each do |node|
353
+ copy_orig_attributes.call(node, "//#{node.name}[@name = '#{node['name']}']")
354
+ end
355
+ # 3. Process copies for `string` nodes
356
+ translated_xml.xpath('//string[@name]').each do |string_node|
357
+ apply_substitutions!(string_node)
358
+ quick_lint(string_node, locale_code)
359
+ end
360
+ # 4. Process copies for `string-array/item` nodes
361
+ translated_xml.xpath('//string-array[@name]/item').each do |item_node|
362
+ apply_substitutions!(item_node)
363
+ quick_lint(item_node, locale_code)
364
+ end
365
+ # 5. Replicate attributes + Process copies for `plurals/item` nodes
366
+ translated_xml.xpath('//plurals[@name]/item[@quantity]').each do |item_node|
367
+ copy_orig_attributes.call(item_node, "//plurals[@name = '#{item_node.parent['name']}']/item[@quantity = '#{item_node['quantity']}']")
368
+ apply_substitutions!(item_node)
369
+ quick_lint(item_node, locale_code)
370
+ end
371
+ end
372
+ private_class_method :post_process_xml!
373
+
374
+ # Apply some common text substitutions to tag contents, like `... => …` or en-dash instead of regular dash for ranges of numbers
327
375
  #
328
376
  # @param [Nokogiri::XML::Node] tag The XML tag/node to apply substitutions to
329
377
  #
330
- def self.apply_substitutions(tag)
378
+ def self.apply_substitutions!(tag)
331
379
  tag.content = tag.content.gsub('...', '…')
332
380
 
333
381
  # Typography en-dash
@@ -339,17 +387,28 @@ module Fastlane
339
387
  is_negative_number ? str : "#{match[1]}\u{2013}#{match[2]}"
340
388
  end
341
389
  end
342
- private_class_method :apply_substitutions
390
+ private_class_method :apply_substitutions!
343
391
 
344
- # Perform some quick basic checks about an individual `<string>` tag and print warnings accordingly
392
+ # Perform some quick basic checks about an individual `<string>` tag and print warnings accordingly:
393
+ # - detect the use of `%%` in the string even if `formatted=false` is set
394
+ # - detect the presence of `\@string/` in translated XML, which suggests the original key that referenced `@string/…` did not set `translatable=false`
395
+ # and thus that `@string/…` copy was sent to GlotPress for translation, then escaped during exporting it back.
345
396
  #
346
- # @param [Nokogiri::XML::Node] string_tag The XML tag/node to check
397
+ # @param [Nokogiri::XML::Node] node The XML tag/node to check the content of
347
398
  # @param [String] lang The language we are currently processing. Used for providing context during logging / warning message
348
399
  #
349
- def self.quick_lint(string_tag, lang)
350
- return unless string_tag['formatted'] == 'false' && string_tag.content.include?('%%')
351
-
352
- UI.important "Warning: [#{lang}] translation for '#{string_tag['name']}' has attribute formatted=false, but still contains escaped '%%' in translation."
400
+ def self.quick_lint(node, lang)
401
+ named_node = node.has_attribute?('name') ? node : node.parent
402
+ if named_node['formatted'] == 'false' && node.content.include?('%%')
403
+ UI.important "Warning: [#{lang}] translation for '#{named_node['name']}' has attribute formatted=false, but still contains escaped '%%' in translation."
404
+ end
405
+ # rubocop:disable Style/GuardClause
406
+ if node.content.include?('\\@string/')
407
+ UI.important "Warning: [#{lang}] exported translation for '#{named_node['name']}' contains `\\@string/`. This is a sign that this entry was not marked as `translatable=false` " \
408
+ + 'in the original `values/strings.xml`, and was thus sent to GlotPress, which added the backslash when exporting it back.'
409
+ node.content = node.content.gsub('\\@string/', '@string/')
410
+ end
411
+ # rubocop:enable Style/GuardClause
353
412
  end
354
413
  private_class_method :quick_lint
355
414
 
@@ -179,6 +179,21 @@ module Fastlane
179
179
  Action.sh('git', 'fetch', '--tags')
180
180
  end
181
181
 
182
+ # Checks if two git references point to the same commit.
183
+ #
184
+ # @param ref1 [String] the first git reference to check.
185
+ # @param ref2 [String] the second git reference to check.
186
+ #
187
+ # @return [Boolean] true if the two references point to the same commit, false otherwise.
188
+ #
189
+ def self.point_to_same_commit?(ref1, ref2)
190
+ git_repo = Git.open(Dir.pwd)
191
+ ref1_commit = git_repo.gcommit(ref1)
192
+ ref2_commit = git_repo.gcommit(ref2)
193
+
194
+ ref1_commit.sha == ref2_commit.sha
195
+ end
196
+
182
197
  # Returns the current git branch, or "HEAD" if it's not checked out to any branch
183
198
  # Can NOT be replaced using the environment variables such as `GIT_BRANCH` or `BUILDKITE_BRANCH`
184
199
  #
@@ -206,6 +221,17 @@ module Fastlane
206
221
  !Action.sh('git', 'branch', '--list', branch_name).empty?
207
222
  end
208
223
 
224
+ # Checks if a branch exists on the repository's remote.
225
+ #
226
+ # @param branch_name [String] the name of the branch to check.
227
+ # @param remote_name [String] the name of the remote repository (default is 'origin').
228
+ #
229
+ # @return [Boolean] true if the branch exists on remote, false otherwise.
230
+ #
231
+ def self.branch_exists_on_remote?(branch_name:, remote_name: 'origin')
232
+ !Action.sh('git', 'ls-remote', '--heads', remote_name, branch_name).empty?
233
+ end
234
+
209
235
  # Ensure that we are on the expected branch, and abort if not.
210
236
  #
211
237
  # @param [String] branch_name The name of the branch we expect to be on
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Fastlane
4
4
  module Wpmreleasetoolkit
5
- VERSION = '11.0.2'
5
+ VERSION = '11.1.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastlane-plugin-wpmreleasetoolkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 11.0.2
4
+ version: 11.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Automattic
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-06-04 00:00:00.000000000 Z
11
+ date: 2024-07-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -449,6 +449,7 @@ files:
449
449
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/copy_branch_protection_action.rb
450
450
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_new_milestone_action.rb
451
451
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_release_action.rb
452
+ - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_release_backmerge_pull_request_action.rb
452
453
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/extract_release_notes_for_version_action.rb
453
454
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/find_previous_tag.rb
454
455
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/firebase_login.rb