fastlane-plugin-wpmreleasetoolkit 5.6.0 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (20) hide show
  1. checksums.yaml +4 -4
  2. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_download_file_by_version.rb +3 -1
  3. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_firebase_test.rb +31 -4
  4. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/close_milestone_action.rb +5 -2
  5. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/comment_on_pr.rb +3 -7
  6. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_new_milestone_action.rb +17 -2
  7. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_release_action.rb +3 -1
  8. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/get_prs_list_action.rb +3 -1
  9. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/promo_screenshots_action.rb +14 -9
  10. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/removebranchprotection_action.rb +12 -6
  11. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/setbranchprotection_action.rb +12 -5
  12. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/setfrozentag_action.rb +5 -2
  13. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/upload_to_s3.rb +16 -1
  14. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_lint_localizations.rb +17 -15
  15. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb +110 -46
  16. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb +24 -4
  17. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_strings_file_validation_helper.rb +14 -11
  18. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/promo_screenshots_helper.rb +58 -10
  19. data/lib/fastlane/plugin/wpmreleasetoolkit/version.rb +1 -1
  20. metadata +2 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd2b56e6aa5cf05e5e464b66e8b136e05048c7cdadee95b43bb152ab6d451e9f
4
- data.tar.gz: da440f83d0f87a6d2dd6c74684a149d381f157d0f0ce40157c12cd237ebed373
3
+ metadata.gz: a5ccc5cff47b793af8f4ab760dae506824b97736743751874e6ffe05da49c6d6
4
+ data.tar.gz: b505f57985fa018c5845437e38b9d62a82db73252fca4990d30bf2e5b8b6455e
5
5
  SHA512:
6
- metadata.gz: e684c1a705d0fab1f9d3ce82f7feed96be8a4aa09a954a19db1a564c4b958654668a6d2b9c23ee41bdcfd39ec68104f3e44e877a8df6d2bb5773efbe706c1730
7
- data.tar.gz: 9fb910e098bbcc54a8753aebb1c2776e0c6e889d20e042f2848a38a5e3ebbeb95ae7b8def13d4efa0fa0e06c050e24e3d80633642a4281e3ba194e55c148427b
6
+ metadata.gz: ef7e10048e6a7bd1795667f06e59ff4af01638ea2a3f47bb2742f3ea50bd1d52ae1c8b747e4fc8d2422599f216e9dc03ba334ace07f9a90dfa785630f937c9d7
7
+ data.tar.gz: 559bee78785335bcc564e04cafc52af9d5514f2d979598ce05c5837ba97b6378c70f0d919809fc6f4db5ca4e5f64db47ac90e430b058d5cd6d777ebf81cc54dc
@@ -9,7 +9,8 @@ module Fastlane
9
9
  UI.user_error!("Can't find any reference for key #{params[:import_key]}") if version.nil?
10
10
  UI.message "Downloading #{params[:file_path]} from #{params[:repository]} at version #{version} to #{params[:download_folder]}"
11
11
 
12
- Fastlane::Helper::GithubHelper.download_file_from_tag(
12
+ github_helper = Fastlane::Helper::GithubHelper.new(github_token: params[:github_token])
13
+ github_helper.download_file_from_tag(
13
14
  repository: params[:repository],
14
15
  tag: "#{params[:github_release_prefix]}#{version}",
15
16
  file_path: params[:file_path],
@@ -57,6 +58,7 @@ module Fastlane
57
58
  description: 'The prefix which is used in the GitHub release title',
58
59
  type: String,
59
60
  optional: true),
61
+ Fastlane::Helper::GithubHelper.github_token_config_item,
60
62
  ]
61
63
  end
62
64
 
@@ -3,8 +3,9 @@ require 'securerandom'
3
3
  module Fastlane
4
4
  module Actions
5
5
  module SharedValues
6
- FIREBASE_TEST_RESULT = :FIREBASE_TEST_LOG_FILE
6
+ FIREBASE_TEST_RESULT = :FIREBASE_TEST_LOG_FILE # FirebaseTestLabResult object, for internal consumption
7
7
  FIREBASE_TEST_LOG_FILE_PATH = :FIREBASE_TEST_LOG_FILE_PATH
8
+ FIREBASE_TEST_MORE_DETAILS_URL = :FIREBASE_TEST_MORE_DETAILS_URL
8
9
  end
9
10
 
10
11
  class AndroidFirebaseTestAction < Action
@@ -45,13 +46,20 @@ module Fastlane
45
46
  key_file_path: params[:key_file]
46
47
  )
47
48
 
48
- FastlaneCore::UI.test_failure! "Firebase Tests failed – more information can be found at #{result.more_details_url}" unless result.success?
49
+ Fastlane::Actions.lane_context[SharedValues::FIREBASE_TEST_MORE_DETAILS_URL] = result.more_details_url
49
50
 
50
- UI.success 'Firebase Tests Complete'
51
+ if result.success?
52
+ UI.success 'Firebase Tests Complete'
53
+ return true
54
+ else
55
+ ui_method = params[:crash_on_test_failure] ? :test_failure! : :error
56
+ FastlaneCore::UI.send(ui_method, "Firebase Tests failed – more information can be found at #{result.more_details_url}")
57
+ return false
58
+ end
51
59
  end
52
60
 
53
61
  # Fastlane doesn't eagerly validate options for us, so we'll do it first to have control over
54
- # when they're evalutated.
62
+ # when they're evaluated.
55
63
  def self.validate_options(params)
56
64
  available_options
57
65
  .reject { |opt| opt.optional || !opt.default_value.nil? }
@@ -180,9 +188,28 @@ module Fastlane
180
188
  optional: true,
181
189
  type: String
182
190
  ),
191
+ FastlaneCore::ConfigItem.new(
192
+ key: :crash_on_test_failure,
193
+ description: 'If set to `true` (the default), will stop fastlane with `test_failure!`. ' \
194
+ + 'If `false`, the action will return the test status, without interrupting the rest of your Fastlane run on failure, letting the caller handle the failure on their side',
195
+ optional: true,
196
+ type: Boolean,
197
+ default_value: true
198
+ ),
183
199
  ]
184
200
  end
185
201
 
202
+ def self.output
203
+ [
204
+ ['FIREBASE_TEST_LOG_FILE_PATH', 'Path to the `output.log` file containing the logs or invoking the tests'],
205
+ ['FIREBASE_TEST_MORE_DETAILS_URL', 'URL to the Firebase Console dashboard showing the details of the test run (and failures, if any)'],
206
+ ]
207
+ end
208
+
209
+ def self.return_value
210
+ 'True if the test succeeded, false if they failed'
211
+ end
212
+
186
213
  def self.authors
187
214
  ['Automattic']
188
215
  end
@@ -10,10 +10,12 @@ module Fastlane
10
10
  repository = params[:repository]
11
11
  milestone_title = params[:milestone]
12
12
 
13
- milestone = Fastlane::Helper::GithubHelper.get_milestone(repository, milestone_title)
13
+ github_helper = Fastlane::Helper::GithubHelper.new(github_token: params[:github_token])
14
+ milestone = github_helper.get_milestone(repository, milestone_title)
15
+
14
16
  UI.user_error!("Milestone #{milestone_title} not found.") if milestone.nil?
15
17
 
16
- Fastlane::Helper::GithubHelper.github_client().update_milestone(repository, milestone[:number], state: 'closed')
18
+ github_helper.update_milestone(repository: repository, number: milestone[:number], state: 'closed')
17
19
  end
18
20
 
19
21
  def self.description
@@ -45,6 +47,7 @@ module Fastlane
45
47
  description: 'The GitHub milestone',
46
48
  optional: false,
47
49
  type: String),
50
+ Fastlane::Helper::GithubHelper.github_token_config_item,
48
51
  ]
49
52
  end
50
53
 
@@ -10,7 +10,8 @@ module Fastlane
10
10
  def self.run(params)
11
11
  require_relative '../../helper/github_helper'
12
12
 
13
- reuse_identifier = Fastlane::Helper::GithubHelper.comment_on_pr(
13
+ github_helper = Fastlane::Helper::GithubHelper.new(github_token: params[:github_token])
14
+ reuse_identifier = github_helper.comment_on_pr(
14
15
  project_slug: params[:project],
15
16
  pr_number: params[:pr_number],
16
17
  body: params[:body],
@@ -41,12 +42,7 @@ module Fastlane
41
42
 
42
43
  def self.available_options
43
44
  [
44
- FastlaneCore::ConfigItem.new(
45
- key: :access_token,
46
- env_name: 'GITHUB_TOKEN',
47
- description: 'The GitHub token to use for posting the comment',
48
- type: String
49
- ),
45
+ Fastlane::Helper::GithubHelper.github_token_config_item,
50
46
  FastlaneCore::ConfigItem.new(
51
47
  key: :reuse_identifier,
52
48
  description: 'If provided, the reuse identifier can identify an existing comment to overwrite',
@@ -9,15 +9,29 @@ module Fastlane
9
9
  def self.run(params)
10
10
  repository = params[:repository]
11
11
 
12
- last_stone = Fastlane::Helper::GithubHelper.get_last_milestone(repository)
12
+ github_helper = Fastlane::Helper::GithubHelper.new(github_token: params[:github_token])
13
+ last_stone = github_helper.get_last_milestone(repository)
14
+
13
15
  UI.message("Last detected milestone: #{last_stone[:title]} due on #{last_stone[:due_on]}.")
16
+
14
17
  milestone_duedate = last_stone[:due_on]
15
18
  milestone_duration = params[:milestone_duration]
16
19
  newmilestone_duedate = (milestone_duedate.to_datetime.next_day(milestone_duration).to_time).utc
17
20
  newmilestone_number = Fastlane::Helper::Ios::VersionHelper.calc_next_release_version(last_stone[:title])
18
21
  number_of_days_from_code_freeze_to_release = params[:number_of_days_from_code_freeze_to_release]
22
+ # Because of the app stores review process, we submit the binary 3 days before the intended release date.
23
+ # Using 3 days is mostly for historical reasons, for a long time, we've been submitting apps on Friday and releasing them on Monday.
24
+ days_until_submission = params[:need_appstore_submission] ? (number_of_days_from_code_freeze_to_release - 3) : milestone_duration
25
+
19
26
  UI.message("Next milestone: #{newmilestone_number} due on #{newmilestone_duedate}.")
20
- Fastlane::Helper::GithubHelper.create_milestone(repository, newmilestone_number, newmilestone_duedate, milestone_duration, number_of_days_from_code_freeze_to_release, params[:need_appstore_submission])
27
+
28
+ github_helper.create_milestone(
29
+ repository: repository,
30
+ title: newmilestone_number,
31
+ due_date: newmilestone_duedate,
32
+ days_until_submission: days_until_submission,
33
+ days_until_release: number_of_days_from_code_freeze_to_release
34
+ )
21
35
  end
22
36
 
23
37
  def self.description
@@ -62,6 +76,7 @@ module Fastlane
62
76
  optional: true,
63
77
  is_string: false,
64
78
  default_value: 14),
79
+ Fastlane::Helper::GithubHelper.github_token_config_item,
65
80
  ]
66
81
  end
67
82
 
@@ -21,7 +21,8 @@ module Fastlane
21
21
  UI.user_error!("Can't find file #{file_path}!") unless File.exist?(file_path)
22
22
  end
23
23
 
24
- Fastlane::Helper::GithubHelper.create_release(
24
+ github_helper = Fastlane::Helper::GithubHelper.new(github_token: params[:github_token])
25
+ github_helper.create_release(
25
26
  repository: repository,
26
27
  version: version,
27
28
  target: params[:target],
@@ -82,6 +83,7 @@ module Fastlane
82
83
  optional: true,
83
84
  default_value: false,
84
85
  is_string: false),
86
+ Fastlane::Helper::GithubHelper.github_token_config_item,
85
87
  ]
86
88
  end
87
89
 
@@ -10,7 +10,8 @@ module Fastlane
10
10
  milestone = params[:milestone]
11
11
 
12
12
  # Get commit list
13
- pr_list = Fastlane::Helper::GithubHelper.get_prs_for_milestone(repository, milestone)
13
+ github_helper = Fastlane::Helper::GithubHelper.new(github_token: params[:github_token])
14
+ pr_list = github_helper.get_prs_for_milestone(repository, milestone)
14
15
 
15
16
  File.open(report_path, 'w') do |file|
16
17
  pr_list.each do |data|
@@ -53,6 +54,7 @@ module Fastlane
53
54
  description: 'The name of the milestone we want to fetch the list of PRs for (e.g.: `16.9`)',
54
55
  optional: false,
55
56
  is_string: true),
57
+ Fastlane::Helper::GithubHelper.github_token_config_item,
56
58
  ]
57
59
  end
58
60
 
@@ -11,7 +11,9 @@ module Fastlane
11
11
  UI.message "#{self.check_path(params[:orig_folder])} Original Screenshot Source: #{params[:orig_folder]}"
12
12
  UI.message "#{self.check_path(params[:metadata_folder])} Translation source: #{params[:metadata_folder]}"
13
13
 
14
- config = helper.read_json(params[:config_file])
14
+ config = helper.read_config(params[:config_file])
15
+
16
+ helper.check_fonts_installed!(config: config) unless params[:skip_font_check]
15
17
 
16
18
  translationDirectories = subdirectories_for_path(params[:metadata_folder])
17
19
  imageDirectories = subdirectories_for_path(params[:orig_folder])
@@ -141,31 +143,34 @@ module Fastlane
141
143
  env_name: 'PROMOSS_ORIG',
142
144
  description: 'The directory containing the original screenshots',
143
145
  optional: false,
144
- is_string: true),
146
+ type: String),
145
147
  FastlaneCore::ConfigItem.new(key: :output_folder,
146
148
  env_name: 'PROMOSS_OUTPUT',
147
149
  description: 'The path of the folder to save the promo screenshots',
148
150
  optional: false,
149
- is_string: true),
150
-
151
+ type: String),
151
152
  FastlaneCore::ConfigItem.new(key: :metadata_folder,
152
153
  env_name: 'PROMOSS_METADATA_FOLDER',
153
154
  description: 'The directory containing the translation data',
154
155
  optional: false,
155
- is_string: true),
156
-
156
+ type: String),
157
157
  FastlaneCore::ConfigItem.new(key: :config_file,
158
158
  env_name: 'PROMOSS_CONFIG_FILE',
159
159
  description: 'The path to the file containing the promo screenshot configuration',
160
160
  optional: true,
161
- is_string: true,
161
+ type: String,
162
162
  default_value: 'screenshots.json'),
163
-
164
163
  FastlaneCore::ConfigItem.new(key: :force,
165
164
  env_name: 'PROMOSS_FORCE_CREATION',
166
165
  description: 'Overwrite existing promo screenshots without asking first?',
167
166
  optional: true,
168
- is_string: false,
167
+ type: Boolean,
168
+ default_value: false),
169
+ FastlaneCore::ConfigItem.new(key: :skip_font_check,
170
+ env_name: 'PROMOSS_SKIP_FONT_CHECK',
171
+ description: 'Skip the check verifying that needed fonts are installed and active',
172
+ optional: true,
173
+ type: Boolean,
169
174
  default_value: false),
170
175
  ]
171
176
  end
@@ -7,14 +7,19 @@ module Fastlane
7
7
  def self.run(params)
8
8
  repository = params[:repository]
9
9
  branch_name = params[:branch]
10
- branch_prot = {}
11
10
 
12
11
  branch_url = "https://api.github.com/repos/#{repository}/branches/#{branch_name}"
13
- branch_prot[:restrictions] = { url: "#{branch_url}/protection/restrictions", users_url: "#{branch_url}/protection/restrictions/users", teams_url: "#{branch_url}/protection/restrictions/teams", users: [], teams: [] }
14
- branch_prot[:enforce_admins] = nil
15
- branch_prot[:required_pull_request_reviews] = { url: "#{branch_url}/protection/required_pull_request_reviews", dismiss_stale_reviews: false, require_code_owner_reviews: false }
16
-
17
- Fastlane::Helper::GithubHelper.github_client().unprotect_branch(repository, branch_name, branch_prot)
12
+ restrictions = { url: "#{branch_url}/protection/restrictions", users_url: "#{branch_url}/protection/restrictions/users", teams_url: "#{branch_url}/protection/restrictions/teams", users: [], teams: [] }
13
+ required_pull_request_reviews = { url: "#{branch_url}/protection/required_pull_request_reviews", dismiss_stale_reviews: false, require_code_owner_reviews: false }
14
+
15
+ github_helper = Fastlane::Helper::GithubHelper.new(github_token: params[:github_token])
16
+ github_helper.remove_branch_protection(
17
+ repository: repository,
18
+ branch: branch_name,
19
+ restrictions: restrictions,
20
+ enforce_admins: nil,
21
+ required_pull_request_reviews: required_pull_request_reviews
22
+ )
18
23
  end
19
24
 
20
25
  def self.description
@@ -46,6 +51,7 @@ module Fastlane
46
51
  description: 'The branch to unprotect',
47
52
  optional: false,
48
53
  type: String),
54
+ Fastlane::Helper::GithubHelper.github_token_config_item,
49
55
  ]
50
56
  end
51
57
 
@@ -7,13 +7,19 @@ module Fastlane
7
7
  def self.run(params)
8
8
  repository = params[:repository]
9
9
  branch_name = params[:branch]
10
- branch_prot = {}
11
10
 
12
11
  branch_url = "https://api.github.com/repos/#{repository}/branches/#{branch_name}"
13
- branch_prot[:restrictions] = { url: "#{branch_url}/protection/restrictions", users_url: "#{branch_url}/protection/restrictions/users", teams_url: "#{branch_url}/protection/restrictions/teams", users: [], teams: [] }
14
- branch_prot[:enforce_admins] = nil
15
- branch_prot[:required_pull_request_reviews] = { url: "#{branch_url}/protection/required_pull_request_reviews", dismiss_stale_reviews: false, require_code_owner_reviews: false }
16
- Fastlane::Helper::GithubHelper.github_client().protect_branch(repository, branch_name, branch_prot)
12
+ restrictions = { url: "#{branch_url}/protection/restrictions", users_url: "#{branch_url}/protection/restrictions/users", teams_url: "#{branch_url}/protection/restrictions/teams", users: [], teams: [] }
13
+ required_pull_request_reviews = { url: "#{branch_url}/protection/required_pull_request_reviews", dismiss_stale_reviews: false, require_code_owner_reviews: false }
14
+
15
+ github_helper = Fastlane::Helper::GithubHelper.new(github_token: params[:github_token])
16
+ github_helper.set_branch_protection(
17
+ repository: repository,
18
+ branch: branch_name,
19
+ restrictions: restrictions,
20
+ enforce_admins: nil,
21
+ required_pull_request_reviews: required_pull_request_reviews
22
+ )
17
23
  end
18
24
 
19
25
  def self.description
@@ -45,6 +51,7 @@ module Fastlane
45
51
  description: 'The branch to protect',
46
52
  optional: false,
47
53
  type: String),
54
+ Fastlane::Helper::GithubHelper.github_token_config_item,
48
55
  ]
49
56
  end
50
57
 
@@ -9,7 +9,9 @@ module Fastlane
9
9
  milestone_title = params[:milestone]
10
10
  freeze = params[:freeze]
11
11
 
12
- milestone = Fastlane::Helper::GithubHelper.get_milestone(repository, milestone_title)
12
+ github_helper = Fastlane::Helper::GithubHelper.new(github_token: params[:github_token])
13
+ milestone = github_helper.get_milestone(repository, milestone_title)
14
+
13
15
  UI.user_error!("Milestone #{milestone_title} not found.") if milestone.nil?
14
16
 
15
17
  mile_title = milestone[:title]
@@ -27,7 +29,7 @@ module Fastlane
27
29
  end
28
30
 
29
31
  UI.message("New milestone: #{mile_title}")
30
- Fastlane::Helper::GithubHelper.github_client().update_milestone(repository, milestone[:number], title: mile_title)
32
+ github_helper.update_milestone(repository: repository, number: milestone[:number], title: mile_title)
31
33
  end
32
34
 
33
35
  def self.is_frozen(milestone)
@@ -70,6 +72,7 @@ module Fastlane
70
72
  optional: false,
71
73
  default_value: true,
72
74
  is_string: false),
75
+ Fastlane::Helper::GithubHelper.github_token_config_item,
73
76
  ]
74
77
  end
75
78
 
@@ -20,7 +20,15 @@ module Fastlane
20
20
  key = [file_name_hash, key].join('/')
21
21
  end
22
22
 
23
- UI.user_error!("File already exists in S3 bucket #{bucket} at #{key}") if file_is_already_uploaded?(bucket, key)
23
+ if file_is_already_uploaded?(bucket, key)
24
+ message = "File already exists in S3 bucket #{bucket} at #{key}"
25
+ if params[:skip_if_exists]
26
+ UI.important("#{message}. Skipping upload.")
27
+ return key
28
+ else
29
+ UI.user_error!(message)
30
+ end
31
+ end
24
32
 
25
33
  UI.message("Uploading #{file_path} to: #{key}")
26
34
 
@@ -101,6 +109,13 @@ module Fastlane
101
109
  default_value: true,
102
110
  type: Boolean
103
111
  ),
112
+ FastlaneCore::ConfigItem.new(
113
+ key: :skip_if_exists,
114
+ description: 'If the file already exists in the S3 bucket, skip the upload (and report it in the logs), instead of failing with `user_error!`',
115
+ optional: true,
116
+ default_value: false,
117
+ type: Boolean
118
+ ),
104
119
  ]
105
120
  end
106
121
 
@@ -2,12 +2,11 @@ module Fastlane
2
2
  module Actions
3
3
  class IosLintLocalizationsAction < Action
4
4
  def self.run(params)
5
- violations = Hash.new([])
5
+ violations = nil
6
6
 
7
7
  loop do
8
- # If we did `violations = self.run...` we'd lose the default value for missing key being `[]` that we set above with `Hash.new`.
9
- # We want that default value so that we can use `+=` when adding the duplicate keys violations below.
10
- violations = violations.merge(self.run_linter(params))
8
+ violations = self.run_linter(params)
9
+ violations.default = [] # Set the default value for when querying a missing key
11
10
 
12
11
  if params[:check_duplicate_keys]
13
12
  find_duplicated_keys(params).each do |language, duplicates|
@@ -49,18 +48,21 @@ module Fastlane
49
48
  def self.find_duplicated_keys(params)
50
49
  duplicate_keys = {}
51
50
 
52
- files_to_lint = Dir.chdir(params[:input_dir]) do
53
- Dir.glob('*.lproj/Localizable.strings').map do |file|
54
- {
55
- language: File.basename(File.dirname(file), '.lproj'),
56
- path: File.join(params[:input_dir], file)
57
- }
58
- end
59
- end
60
-
51
+ files_to_lint = Dir.glob('*.lproj/Localizable.strings', base: params[:input_dir])
61
52
  files_to_lint.each do |file|
62
- duplicates = Fastlane::Helper::Ios::StringsFileValidationHelper.find_duplicated_keys(file: file[:path])
63
- duplicate_keys[file[:language]] = duplicates.map { |key, value| "`#{key}` was found at multiple lines: #{value.join(', ')}" } unless duplicates.empty?
53
+ language = File.basename(File.dirname(file), '.lproj')
54
+ path = File.join(params[:input_dir], file)
55
+
56
+ file_type = Fastlane::Helper::Ios::L10nHelper.strings_file_type(path: path)
57
+ if file_type == :text
58
+ duplicates = Fastlane::Helper::Ios::StringsFileValidationHelper.find_duplicated_keys(file: path)
59
+ duplicate_keys[language] = duplicates.map { |key, value| "`#{key}` was found at multiple lines: #{value.join(', ')}" } unless duplicates.empty?
60
+ else
61
+ UI.important <<~WRONG_FORMAT
62
+ File `#{path}` is in #{file_type} format, while finding duplicate keys only make sense on files that are in ASCII-plist format.
63
+ Since your files are in #{file_type} format, you should probably disable the `check_duplicate_keys` option from this `#{self.action_name}` call.
64
+ WRONG_FORMAT
65
+ end
64
66
  end
65
67
 
66
68
  duplicate_keys
@@ -8,34 +8,25 @@ module Fastlane
8
8
 
9
9
  module Helper
10
10
  class GithubHelper
11
- def self.github_token!
12
- token = [
13
- 'GHHELPER_ACCESS', # For historical reasons / backward compatibility
14
- 'GITHUB_TOKEN', # Used by the `gh` CLI tool
15
- ].map { |key| ENV[key] }
16
- .compact
17
- .first
18
-
19
- token || UI.user_error!('Please provide a GitHub authentication token via the `GITHUB_TOKEN` environment variable')
20
- end
21
-
22
- def self.github_client
23
- @@client ||= begin
24
- client = Octokit::Client.new(access_token: github_token!)
11
+ attr_reader :client
25
12
 
26
- # Fetch the current user
27
- user = client.user
28
- UI.message("Logged in as: #{user.name}")
13
+ # Helper for GitHub Actions
14
+ #
15
+ # @param [String?] github_token GitHub OAuth access token
16
+ #
17
+ def initialize(github_token:)
18
+ @client = Octokit::Client.new(access_token: github_token)
29
19
 
30
- # Auto-paginate to ensure we're not missing data
31
- client.auto_paginate = true
20
+ # Fetch the current user
21
+ user = @client.user
22
+ UI.message("Logged in as: #{user.name}")
32
23
 
33
- client
34
- end
24
+ # Auto-paginate to ensure we're not missing data
25
+ @client.auto_paginate = true
35
26
  end
36
27
 
37
- def self.get_milestone(repository, release)
38
- miles = github_client().list_milestones(repository)
28
+ def get_milestone(repository, release)
29
+ miles = client.list_milestones(repository)
39
30
  mile = nil
40
31
 
41
32
  miles&.each do |mm|
@@ -51,15 +42,15 @@ module Fastlane
51
42
  # @param [String] milestone The name of the milestone we want to fetch the list of PRs for (e.g.: `16.9`)
52
43
  # @return [<Sawyer::Resource>] A list of the PRs for the given milestone, sorted by number
53
44
  #
54
- def self.get_prs_for_milestone(repository, milestone)
55
- github_client.search_issues(%(type:pr milestone:"#{milestone}" repo:#{repository}))[:items].sort_by(&:number)
45
+ def get_prs_for_milestone(repository, milestone)
46
+ client.search_issues(%(type:pr milestone:"#{milestone}" repo:#{repository}))[:items].sort_by(&:number)
56
47
  end
57
48
 
58
- def self.get_last_milestone(repository)
49
+ def get_last_milestone(repository)
59
50
  options = {}
60
51
  options[:state] = 'open'
61
52
 
62
- milestones = github_client().list_milestones(repository, options)
53
+ milestones = client.list_milestones(repository, options)
63
54
  return nil if milestones.nil?
64
55
 
65
56
  last_stone = nil
@@ -80,19 +71,42 @@ module Fastlane
80
71
  last_stone
81
72
  end
82
73
 
83
- def self.create_milestone(repository, newmilestone_number, newmilestone_duedate, newmilestone_duration, number_of_days_from_code_freeze_to_release, need_submission)
84
- # If there is a review process, we want to submit the binary 3 days before its release
85
- #
86
- # Using 3 days is mostly for historical reasons where we release the apps on Monday and submit them on Friday.
87
- days_until_submission = need_submission ? (number_of_days_from_code_freeze_to_release - 3) : newmilestone_duration
88
- submission_date = newmilestone_duedate.to_datetime.next_day(days_until_submission)
89
- release_date = newmilestone_duedate.to_datetime.next_day(number_of_days_from_code_freeze_to_release)
90
- comment = "Code freeze: #{newmilestone_duedate.to_datetime.strftime('%B %d, %Y')} App Store submission: #{submission_date.strftime('%B %d, %Y')} Release: #{release_date.strftime('%B %d, %Y')}"
74
+ # Creates a new milestone
75
+ #
76
+ # @param [String] repository The repository name, including the organization (e.g. `wordpress-mobile/wordpress-ios`)
77
+ # @param [String] title The name of the milestone we want to create (e.g.: `16.9`)
78
+ # @param [Time] due_date Milestone due date—which will also correspond to the code freeze date
79
+ # @param [Integer] days_until_submission Number of days from code freeze to submission to the App Store / Play Store
80
+ # @param [Integer] days_until_release Number of days from code freeze to release
81
+ #
82
+ def create_milestone(repository:, title:, due_date:, days_until_submission:, days_until_release:)
83
+ UI.user_error!('days_until_release must be greater than zero.') unless days_until_release.positive?
84
+ UI.user_error!('days_until_submission must be greater than zero.') unless days_until_submission.positive?
85
+ UI.user_error!('days_until_release must be greater or equal to days_until_submission.') unless days_until_release >= days_until_submission
86
+
87
+ submission_date = due_date.to_datetime.next_day(days_until_submission)
88
+ release_date = due_date.to_datetime.next_day(days_until_release)
89
+ comment = <<~MILESTONE_DESCRIPTION
90
+ Code freeze: #{due_date.to_datetime.strftime('%B %d, %Y')}
91
+ App Store submission: #{submission_date.strftime('%B %d, %Y')}
92
+ Release: #{release_date.strftime('%B %d, %Y')}
93
+ MILESTONE_DESCRIPTION
91
94
 
92
95
  options = {}
93
- options[:due_on] = newmilestone_duedate
96
+ # == Workaround for GitHub API bug ==
97
+ #
98
+ # It seems that whatever date we send to the API, GitHub will 'floor' it to the date that seems to be at
99
+ # 00:00 PST/PDT and then discard the time component of the date we sent.
100
+ # This means that, when we cross the November DST change date, where the due date of the previous milestone
101
+ # was e.g. `2022-10-31T07:00:00Z` and `.next_day(14)` returns `2022-11-14T07:00:00Z` and we send that value
102
+ # for the `due_on` field via the API, GitHub ends up creating a milestone with a due of `2022-11-13T08:00:00Z`
103
+ # instead, introducing an off-by-one error on that due date.
104
+ #
105
+ # This is a bug in the GitHub API, not in our date computation logic.
106
+ # To solve this, we trick it by forcing the time component of the ISO date we send to be `12:00:00Z`.
107
+ options[:due_on] = due_date.strftime('%Y-%m-%dT12:00:00Z')
94
108
  options[:description] = comment
95
- github_client().create_milestone(repository, newmilestone_number, options)
109
+ client.create_milestone(repository, title, options)
96
110
  end
97
111
 
98
112
  # Creates a Release on GitHub as a Draft
@@ -106,8 +120,8 @@ module Fastlane
106
120
  # @param [Array<String>] assets List of file paths to attach as assets to the release
107
121
  # @param [TrueClass|FalseClass] prerelease Indicates if this should be created as a pre-release (i.e. for alpha/beta)
108
122
  #
109
- def self.create_release(repository:, version:, target: nil, description:, assets:, prerelease:)
110
- release = github_client().create_release(
123
+ def create_release(repository:, version:, target: nil, description:, assets:, prerelease:)
124
+ release = client.create_release(
111
125
  repository,
112
126
  version, # tag name
113
127
  name: version, # release name
@@ -117,7 +131,7 @@ module Fastlane
117
131
  body: description
118
132
  )
119
133
  assets.each do |file_path|
120
- github_client().upload_asset(release[:url], file_path, content_type: 'application/octet-stream')
134
+ client.upload_asset(release[:url], file_path, content_type: 'application/octet-stream')
121
135
  end
122
136
  end
123
137
 
@@ -129,15 +143,14 @@ module Fastlane
129
143
  # @param [String] download_folder The folder which the file should be downloaded into
130
144
  # @return [String] The path of the downloaded file, or nil if something went wrong
131
145
  #
132
- def self.download_file_from_tag(repository:, tag:, file_path:, download_folder:)
146
+ def download_file_from_tag(repository:, tag:, file_path:, download_folder:)
133
147
  repository = repository.delete_prefix('/').chomp('/')
134
148
  file_path = file_path.delete_prefix('/').chomp('/')
135
149
  file_name = File.basename(file_path)
136
150
  download_path = File.join(download_folder, file_name)
137
151
 
138
- download_url = github_client.contents(repository,
139
- path: file_path,
140
- ref: tag).download_url
152
+ download_url = client.contents(repository, path: file_path, ref: tag).download_url
153
+
141
154
  begin
142
155
  uri = URI.parse(download_url)
143
156
  uri.open do |remote_file|
@@ -151,8 +164,7 @@ module Fastlane
151
164
  end
152
165
 
153
166
  # Creates (or updates an existing) GitHub PR Comment
154
- def self.comment_on_pr(project_slug:, pr_number:, body:, reuse_identifier: SecureRandom.uuid)
155
- client = github_client
167
+ def comment_on_pr(project_slug:, pr_number:, body:, reuse_identifier: SecureRandom.uuid)
156
168
  comments = client.issue_comments(project_slug, pr_number)
157
169
 
158
170
  reuse_marker = "<!-- REUSE_ID: #{reuse_identifier} -->"
@@ -172,6 +184,58 @@ module Fastlane
172
184
 
173
185
  reuse_identifier
174
186
  end
187
+
188
+ # Update a milestone for a repository
189
+ #
190
+ # @param [String] repository The repository name (including the organization)
191
+ # @param [String] number The number of the milestone we want to fetch
192
+ # @param options [Hash] A customizable set of options.
193
+ # @option options [String] :title A unique title.
194
+ # @option options [String] :state
195
+ # @option options [String] :description A meaningful description
196
+ # @option options [Time] :due_on Set if the milestone has a due date
197
+ # @return [Milestone] A single milestone object
198
+ # @see http://developer.github.com/v3/issues/milestones/#update-a-milestone
199
+ #
200
+ def update_milestone(repository:, number:, **options)
201
+ client.update_milestone(repository, number, options)
202
+ end
203
+
204
+ # Remove the protection of a single branch from a repository
205
+ #
206
+ # @param [String] repository The repository name (including the organization)
207
+ # @param [String] branch The branch name
208
+ # @param [Hash] options A customizable set of options.
209
+ # @see https://docs.github.com/en/rest/branches/branch-protection#update-branch-protection
210
+ #
211
+ def remove_branch_protection(repository:, branch:, **options)
212
+ client.unprotect_branch(repository, branch, options)
213
+ end
214
+
215
+ # Protects a single branch from a repository
216
+ #
217
+ # @param [String] repository The repository name (including the organization)
218
+ # @param [String] branch The branch name
219
+ # @param options [Hash] A customizable set of options.
220
+ # @see https://docs.github.com/en/rest/branches/branch-protection#update-branch-protection
221
+ #
222
+ def set_branch_protection(repository:, branch:, **options)
223
+ client.protect_branch(repository, branch, options)
224
+ end
225
+
226
+ # Creates a GithubToken Fastlane ConfigItem
227
+ #
228
+ # @return [FastlaneCore::ConfigItem] The Fastlane ConfigItem for GitHub OAuth access token
229
+ #
230
+ def self.github_token_config_item
231
+ FastlaneCore::ConfigItem.new(
232
+ key: :github_token,
233
+ env_name: 'GITHUB_TOKEN',
234
+ description: 'The GitHub OAuth access token',
235
+ optional: false,
236
+ type: String
237
+ )
238
+ end
175
239
  end
176
240
  end
177
241
  end
@@ -36,6 +36,28 @@ module Fastlane
36
36
  end
37
37
  end
38
38
 
39
+ # Read a file line by line and iterate over it (just like `File.readlines` does),
40
+ # except that it also detects the encoding used by the file (using the BOM if present) when reading it,
41
+ # and then convert each line to UTF-8 before yielding it
42
+ #
43
+ # This is particularly useful if you need to then use a `RegExp` to match part of the lines you're iterating over,
44
+ # as the `RegExp` (which will typically be UTF-8) and the string you're matching with it have to use the same encoding
45
+ # (otherwise we would get a `Encoding::CompatibilityError`)
46
+ #
47
+ # @important If you are then using a `RegExp` to match the UTF-8 lines you iterate on,
48
+ # remember to use the `u` flag on it (`/…/u`) to make it UTF-8-aware too.
49
+ #
50
+ # @param [String] file The path to the file to read
51
+ # @yield each line read from the file, after converting it to the UTF-8 encoding
52
+ #
53
+ def self.read_utf8_lines(file)
54
+ # Be sure to guess file encoding using the Byte-Order-Mark, and fallback to UTF-8 if there's no BOM.
55
+ File.readlines(file, mode: 'rb:BOM|UTF-8').map do |line|
56
+ # Ensure the line is re-encoded to UTF-8 regardless of the encoding that was used in the input file
57
+ line.encode(Encoding::UTF_8)
58
+ end
59
+ end
60
+
39
61
  # Merge the content of multiple `.strings` files into a new `.strings` text file.
40
62
  #
41
63
  # @param [Hash<String, String>] paths The paths of the `.strings` files to merge together, associated with the prefix to prepend to each of their respective keys
@@ -68,11 +90,9 @@ module Fastlane
68
90
  all_keys_found += string_keys
69
91
 
70
92
  tmp_file.write("/* MARK: - #{File.basename(input_file)} */\n\n")
71
- # Read line-by-line to reduce memory footprint during content copy; Be sure to guess file encoding using the Byte-Order-Mark.
72
- File.readlines(input_file, mode: 'rb:BOM|UTF-8').each do |line|
93
+ # Read line-by-line to reduce memory footprint during content copy
94
+ read_utf8_lines(input_file).each do |line|
73
95
  unless prefix.nil? || prefix.empty?
74
- # We need to ensure the line and RegExp are using the same encoding, so we transcode everything to UTF-8.
75
- line.encode!(Encoding::UTF_8)
76
96
  # The `/u` modifier on the RegExps is to make them UTF-8
77
97
  line.gsub!(/^(\s*")/u, "\\1#{prefix}") # Lines starting with a quote are considered to be start of a key; add prefix right after the quote
78
98
  line.gsub!(/^(\s*)([A-Z0-9_]+)(\s*=\s*")/ui, "\\1\"#{prefix}\\2\"\\3") # Lines starting with an identifier followed by a '=' are considered to be an unquoted key (typical in InfoPlist.strings files for example)
@@ -11,25 +11,25 @@ module Fastlane
11
11
 
12
12
  TRANSITIONS = {
13
13
  root: {
14
- /\s/ => :root,
14
+ /\s/u => :root,
15
15
  '/' => :maybe_comment_start,
16
16
  '"' => :in_quoted_key
17
17
  },
18
18
  maybe_comment_start: {
19
19
  '/' => :in_line_comment,
20
- /\*/ => :in_block_comment
20
+ /\*/u => :in_block_comment
21
21
  },
22
22
  in_line_comment: {
23
23
  "\n" => :root,
24
- /./ => :in_line_comment
24
+ /./u => :in_line_comment
25
25
  },
26
26
  in_block_comment: {
27
27
  /\*/ => :maybe_block_comment_end,
28
- /./m => :in_block_comment
28
+ /./mu => :in_block_comment
29
29
  },
30
30
  maybe_block_comment_end: {
31
31
  '/' => :root,
32
- /./m => :in_block_comment
32
+ /./mu => :in_block_comment
33
33
  },
34
34
  in_quoted_key: {
35
35
  '"' => lambda do |state, _|
@@ -37,25 +37,25 @@ module Fastlane
37
37
  state.buffer.string = ''
38
38
  :after_quoted_key_before_eq
39
39
  end,
40
- /./ => lambda do |state, c|
40
+ /./u => lambda do |state, c|
41
41
  state.buffer.write(c)
42
42
  :in_quoted_key
43
43
  end
44
44
  },
45
45
  after_quoted_key_before_eq: {
46
- /\s/ => :after_quoted_key_before_eq,
46
+ /\s/u => :after_quoted_key_before_eq,
47
47
  '=' => :after_quoted_key_and_eq
48
48
  },
49
49
  after_quoted_key_and_eq: {
50
- /\s/ => :after_quoted_key_and_eq,
50
+ /\s/u => :after_quoted_key_and_eq,
51
51
  '"' => :in_quoted_value
52
52
  },
53
53
  in_quoted_value: {
54
54
  '"' => :after_quoted_value,
55
- /./m => :in_quoted_value
55
+ /./mu => :in_quoted_value
56
56
  },
57
57
  after_quoted_value: {
58
- /\s/ => :after_quoted_value,
58
+ /\s/u => :after_quoted_value,
59
59
  ';' => :root
60
60
  }
61
61
  }.freeze
@@ -70,7 +70,10 @@ module Fastlane
70
70
 
71
71
  state = State.new(context: :root, buffer: StringIO.new, in_escaped_ctx: false, found_key: nil)
72
72
 
73
- File.readlines(file).each_with_index do |line, line_no|
73
+ # Using our `each_utf8_line` helper instead of `File.readlines` ensures we can also read files that are
74
+ # encoded in UTF-16, yet process each of their lines as a UTF-8 string, so that `RegExp#match?` don't throw
75
+ # an `Encoding::CompatibilityError` exception. (Note how all our `RegExp`s in `TRANSITIONS` have the `u` flag)
76
+ Fastlane::Helper::Ios::L10nHelper.read_utf8_lines(file).each_with_index do |line, line_no|
74
77
  line.chars.each_with_index do |c, col_no|
75
78
  # Handle escaped characters at a global level.
76
79
  # This is more straightforward than having to account for it in the `TRANSITIONS` table.
@@ -7,11 +7,11 @@ rescue LoadError
7
7
  end
8
8
  require 'json'
9
9
  require 'tempfile'
10
+ require 'open3'
10
11
  require 'optparse'
11
12
  require 'pathname'
12
13
  require 'progress_bar'
13
14
  require 'parallel'
14
- require 'jsonlint'
15
15
  require 'chroma'
16
16
  require 'securerandom'
17
17
 
@@ -32,17 +32,65 @@ module Fastlane
32
32
  UI.user_error!('`drawText` not found – install it using `brew install automattic/build-tools/drawText`.') unless system('command -v drawText')
33
33
  end
34
34
 
35
- def read_json(configFilePath)
35
+ def read_config(configFilePath)
36
36
  configFilePath = resolve_path(configFilePath)
37
37
 
38
38
  begin
39
- return JSON.parse(open(configFilePath).read)
40
- rescue
41
- linter = JsonLint::Linter.new
42
- linter.check(configFilePath)
43
- linter.display_errors
39
+ # NOTE: While JSON is a subset of YAML and thus YAML.load_file would technically cover both cases at once, in practice
40
+ # `JSON.parse` is more lenient with JSON files than `YAML.load_file` is — especially, it accepts `// comments` in the
41
+ # JSON file, despite this not being allowed in the spec — hence why we still try with `JSON.parse` for `.json` files.
42
+ return File.extname(configFilePath) == '.json' ? JSON.parse(File.read(configFilePath)) : YAML.load_file(configFilePath)
43
+ rescue StandardError => e
44
+ UI.error(e)
45
+ UI.user_error!('Invalid JSON/YAML configuration. Please lint your config file to check for syntax errors.')
46
+ end
47
+ end
44
48
 
45
- UI.user_error!('Invalid JSON configuration. See errors in log.')
49
+ # Checks that all required fonts are installed
50
+ # - Visits the JSON config to find all the stylesheets referenced in it
51
+ # - For each stylesheet, extract the first font of each `font-family` attribute found
52
+ # - Finally, for each of those fonts, check that they exist and are activated.
53
+ #
54
+ # @param [Hash] config The promo screenshots configuration, as returned by #read_config
55
+ # @raise [UserError] Raises if at least one font is missing.
56
+ #
57
+ def check_fonts_installed!(config:)
58
+ # Find all stylesheets in the config
59
+ all_stylesheets = ([config['stylesheet']] + config['entries'].flat_map do |entry|
60
+ entry['attachments']&.map { |att| att['stylesheet'] }
61
+ end).compact.uniq
62
+
63
+ # Parse the first font in each `font-family` attribute found in all found CSS files.
64
+ # Only the first in each `font-family` font list matters, as others are fallbacks we don't want to use anyway.
65
+ font_families = all_stylesheets.flat_map do |s|
66
+ File.readlines(s).flat_map do |line|
67
+ attr = line.match(/font-family: (.*);/)&.captures&.first
68
+ attr.split(',').first.strip.gsub(/'(.*)'/, '\1').gsub(/"(.*)"/, '\1') unless attr.nil?
69
+ end
70
+ end.compact.uniq
71
+
72
+ # Verify that all fonts exists and are active—using a small swift script as there's no nice CLI for that
73
+ swift_script = <<~SWIFT
74
+ import AppKit
75
+
76
+ var exitCode: Int32 = 0
77
+ for fontName in CommandLine.arguments.dropFirst() {
78
+ if NSFont(name: fontName, size: NSFont.systemFontSize) != nil {
79
+ print(" ✅ Font \\"\\(fontName)\\" found and active")
80
+ } else {
81
+ print(" ❌ Font \\"\\(fontName)\\" not found, it is either not installed or disabled. Please install it using FontBook first.")
82
+ exitCode = 1
83
+ }
84
+ }
85
+ exit(exitCode)
86
+ SWIFT
87
+
88
+ Tempfile.create(['fonts-check-', '.swift']) do |f|
89
+ f.write(swift_script)
90
+ f.close
91
+ oe, s = Open3.capture2e('/usr/bin/env', 'xcrun', 'swift', f.path, *font_families)
92
+ UI.command_output(oe)
93
+ UI.user_error!('Some fonts required by your stylesheets are missing. Please install them and try again.') unless s.success?
46
94
  end
47
95
  end
48
96
 
@@ -343,8 +391,8 @@ module Fastlane
343
391
  def open_image(path)
344
392
  path = resolve_path(path)
345
393
 
346
- Magick::Image.read(path) do
347
- self.background_color = 'transparent'
394
+ Magick::Image.read(path) do |image|
395
+ image.background_color = 'transparent'
348
396
  end.first
349
397
  end
350
398
 
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module Wpmreleasetoolkit
3
- VERSION = '5.6.0'
3
+ VERSION = '6.1.0'
4
4
  end
5
5
  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: 5.6.0
4
+ version: 6.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: 2022-09-27 00:00:00.000000000 Z
11
+ date: 2022-12-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -80,20 +80,6 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '1.3'
83
- - !ruby/object:Gem::Dependency
84
- name: jsonlint
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: '0.3'
90
- type: :runtime
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "~>"
95
- - !ruby/object:Gem::Version
96
- version: '0.3'
97
83
  - !ruby/object:Gem::Dependency
98
84
  name: nokogiri
99
85
  requirement: !ruby/object:Gem::Requirement