fastlane-plugin-wpmreleasetoolkit 5.5.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. checksums.yaml +4 -4
  2. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_create_avd_action.rb +90 -0
  3. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_download_file_by_version.rb +3 -1
  4. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_launch_emulator_action.rb +61 -0
  5. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb +6 -11
  6. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_shutdown_emulator_action.rb +48 -0
  7. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/close_milestone_action.rb +5 -2
  8. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/comment_on_pr.rb +3 -7
  9. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_new_milestone_action.rb +17 -2
  10. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_release_action.rb +3 -1
  11. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/get_prs_list_action.rb +3 -1
  12. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/removebranchprotection_action.rb +12 -6
  13. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/setbranchprotection_action.rb +12 -5
  14. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/setfrozentag_action.rb +5 -2
  15. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/upload_to_s3.rb +16 -1
  16. data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_lint_localizations.rb +17 -15
  17. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_emulator_helper.rb +198 -0
  18. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_tools_path_helper.rb +87 -0
  19. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb +110 -46
  20. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb +24 -4
  21. data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_strings_file_validation_helper.rb +14 -11
  22. data/lib/fastlane/plugin/wpmreleasetoolkit/version.rb +1 -1
  23. metadata +9 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 858fcaeb7ccaa13b2b3990d36aa1c9aa15f9f9cb90c7ee7bb6941442f2c97847
4
- data.tar.gz: '067942032d28224600deae4333b427e3eda6af56d65fb83eefb5ef4e3c07b868'
3
+ metadata.gz: c16be217cbd668e4fbd9da107edcdba4b5be149f0d64d4dadb6a0d5697cde006
4
+ data.tar.gz: e736842ca7df0e1ff50fc0a7114e39555570c03c8e58b41fa265999f267f0ec2
5
5
  SHA512:
6
- metadata.gz: eb6a9f16072c66b7458d4a0787b300cdddd0d87ae99cc886ecc5d64a733d11f046a27c6167b7150fd73c358d9f6c3c85e03b10d4b5c76202fa5273560b5b523e
7
- data.tar.gz: ca606aef9d929fe6ae3c1f821575e07fc4b672929b61ad7174a1f7e43dea8c2698ed63c696c62506beca226581f726fccadc01930e15b5e8088aa7908e2f0a6c
6
+ metadata.gz: 739938930d2cfd9238590f6b58b7031923f5f6b325c53246058f77dc47c7e302180f2d088b8c764106d50e45f7bca265d915a3d671bbf09c6a45e9309fcb1e3e
7
+ data.tar.gz: 0741aeff083cab3060b8b4f46259ad091217eef34fd013370fca26289b5c2279c325632dd7935699772e293e81e87ffcbd71bc7b8905da5fffbb4e04543f61e5
@@ -0,0 +1,90 @@
1
+ module Fastlane
2
+ module Actions
3
+ class AndroidCreateAvdAction < Action
4
+ def self.run(params)
5
+ device_model = params[:device_model]
6
+ api_level = params[:api_level]
7
+ avd_name = params[:avd_name]
8
+ sdcard = params[:sdcard]
9
+
10
+ helper = Fastlane::Helper::Android::EmulatorHelper.new
11
+
12
+ # Ensure we have the system image needed for creating the AVD with this API level
13
+ system_image = params[:system_image] || helper.install_system_image(api: api_level)
14
+
15
+ # Create the AVD for device, API and system image we need
16
+ helper.create_avd(
17
+ api: api_level,
18
+ device: device_model,
19
+ system_image: system_image,
20
+ name: avd_name,
21
+ sdcard: sdcard
22
+ )
23
+ end
24
+
25
+ #####################################################
26
+ # @!group Documentation
27
+ #####################################################
28
+
29
+ def self.description
30
+ 'Creates a new Android Virtual Device (AVD) for a specific device model and API level'
31
+ end
32
+
33
+ def self.details
34
+ <<~DESC
35
+ Creates a new Android Virtual Device (AVD) for a specific device model and API level.
36
+ By default, it also installs the necessary system image (using `sdkmanager`) if needed before creating the AVD
37
+ DESC
38
+ end
39
+
40
+ def self.available_options
41
+ [
42
+ FastlaneCore::ConfigItem.new(key: :device_model,
43
+ env_name: 'FL_ANDROID_CREATE_AVD_DEVICE_MODEL',
44
+ description: 'The device model code to use to create the AVD. Valid values can be found using `avdmanager list devices`',
45
+ type: String,
46
+ optional: false),
47
+ FastlaneCore::ConfigItem.new(key: :api_level,
48
+ env_name: 'FL_ANDROID_CREATE_AVD_API_LEVEL',
49
+ description: 'The API level to use to install the necessary system-image and create the AVD',
50
+ type: Integer,
51
+ optional: false),
52
+ FastlaneCore::ConfigItem.new(key: :avd_name,
53
+ env_name: 'FL_ANDROID_CREATE_AVD_AVD_NAME',
54
+ description: 'The name to give to the created AVD. If not provided, will be derived from device model and API level',
55
+ type: String,
56
+ optional: true,
57
+ default_value: nil),
58
+ FastlaneCore::ConfigItem.new(key: :sdcard,
59
+ env_name: 'FL_ANDROID_CREATE_AVD_SDCARD',
60
+ description: 'The size of the SD card to use for the AVD',
61
+ type: String,
62
+ optional: true,
63
+ default_value: '512M'),
64
+ FastlaneCore::ConfigItem.new(key: :system_image,
65
+ env_name: 'FL_ANDROID_CREATE_AVD_SYSTEM_IMAGE',
66
+ description: 'The system image to use (as used/listed by `sdkmanager`). Defaults to the appropriate system image given the API level requested and the current machine\'s architecture',
67
+ type: String,
68
+ optional: true,
69
+ default_value_dynamic: true,
70
+ default_value: nil),
71
+ ]
72
+ end
73
+
74
+ def self.output
75
+ end
76
+
77
+ def self.return_value
78
+ 'Returns the name of the created AVD'
79
+ end
80
+
81
+ def self.authors
82
+ ['Automattic']
83
+ end
84
+
85
+ def self.is_supported?(platform)
86
+ platform == :android
87
+ end
88
+ end
89
+ end
90
+ end
@@ -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
 
@@ -0,0 +1,61 @@
1
+ module Fastlane
2
+ module Actions
3
+ class AndroidLaunchEmulatorAction < Action
4
+ def self.run(params)
5
+ helper = Fastlane::Helper::Android::EmulatorHelper.new
6
+ helper.launch_avd(
7
+ name: params[:avd_name],
8
+ cold_boot: params[:cold_boot],
9
+ wipe_data: params[:wipe_data]
10
+ )
11
+ end
12
+
13
+ #####################################################
14
+ # @!group Documentation
15
+ #####################################################
16
+
17
+ def self.description
18
+ 'Boots an Android emulator using the given AVD name'
19
+ end
20
+
21
+ def self.details
22
+ description
23
+ end
24
+
25
+ def self.available_options
26
+ [
27
+ FastlaneCore::ConfigItem.new(key: :avd_name,
28
+ env_name: 'FL_ANDROID_LAUNCH_EMULATOR_AVD_NAME',
29
+ description: 'The name of the AVD to boot',
30
+ type: String,
31
+ optional: false),
32
+ FastlaneCore::ConfigItem.new(key: :cold_boot,
33
+ env_name: 'FL_ANDROID_LAUNCH_EMULATOR_COLD_BOOT',
34
+ description: 'Indicate if we want a cold boot (true) of if we prefer booting from a snapshot (false)',
35
+ type: Fastlane::Boolean,
36
+ default_value: true),
37
+ FastlaneCore::ConfigItem.new(key: :wipe_data,
38
+ env_name: 'FL_ANDROID_LAUNCH_EMULATOR_WIPE_DATA',
39
+ description: 'Indicate if we want to wipe the device data before booting the AVD, so it is like it were a brand new device',
40
+ type: Fastlane::Boolean,
41
+ default_value: true),
42
+ ]
43
+ end
44
+
45
+ def self.output
46
+ end
47
+
48
+ def self.return_value
49
+ 'The serial of the emulator that was created after booting the AVD (e.g. `emulator-5554`)'
50
+ end
51
+
52
+ def self.authors
53
+ ['Automattic']
54
+ end
55
+
56
+ def self.is_supported?(platform)
57
+ platform == :android
58
+ end
59
+ end
60
+ end
61
+ end
@@ -71,31 +71,26 @@ module Fastlane
71
71
  end
72
72
 
73
73
  # The path where the `apkanalyzer` binary was found, after searching it:
74
- # - in priority in `$ANDROID_SDK_ROOT` (or `$ANDROID_HOME` for legacy setups), under `cmdline-tools/latest/bin/` or `cmdline-tools/tools/bin`
74
+ # - in priority in `$ANDROID_HOME` (or `$ANDROID_SDK_ROOT` for legacy setups), under `cmdline-tools/latest/bin/` or `cmdline-tools/tools/bin`
75
75
  # - and falling back by trying to find it in `$PATH`
76
76
  #
77
77
  # @return [String,Nil] The path to `apkanalyzer`, or `nil` if it wasn't found in any of the above tested paths.
78
78
  #
79
79
  def find_apkanalyzer_binary
80
- sdk_root = ENV['ANDROID_SDK_ROOT'] || ENV['ANDROID_HOME']
81
- if sdk_root
82
- pattern = File.join(sdk_root, 'cmdline-tools', '{latest,tools}', 'bin', 'apkanalyzer')
83
- apkanalyzer_bin = Dir.glob(pattern).find { |path| File.executable?(path) }
84
- end
85
- apkanalyzer_bin || Action.sh('command', '-v', 'apkanalyzer', print_command_output: false) { |_| nil }
80
+ @tools ||= Fastlane::Helper::Android::ToolsPathHelper.new
81
+ @tools.find_tool_path(binary: 'apkanalyzer', search_paths: @tools.cmdline_tools_search_paths)
86
82
  end
87
83
 
88
84
  # The path where the `apkanalyzer` binary was found, after searching it:
89
- # - in priority in `$ANDROID_SDK_ROOT` (or `$ANDROID_HOME` for legacy setups), under `cmdline-tools/latest/bin/` or `cmdline-tools/tools/bin`
85
+ # - in priority in `$ANDROID_HOME` (or `$ANDROID_SDK_ROOT` for legacy setups), under `cmdline-tools/latest/bin/` or `cmdline-tools/tools/bin`
90
86
  # - and falling back by trying to find it in `$PATH`
91
87
  #
92
88
  # @return [String] The path to `apkanalyzer`
93
89
  # @raise [FastlaneCore::Interface::FastlaneError] if it wasn't found in any of the above tested paths.
94
90
  #
95
91
  def find_apkanalyzer_binary!
96
- apkanalyzer_bin = find_apkanalyzer_binary
97
- UI.user_error!('Unable to find `apkanalyzer` executable in either `$PATH` or `$ANDROID_SDK_ROOT`. Make sure you installed the Android SDK Command-line Tools') if apkanalyzer_bin.nil?
98
- apkanalyzer_bin
92
+ @tools ||= Fastlane::Helper::Android::ToolsPathHelper.new
93
+ @tools.find_tool_path!(binary: 'apkanalyzer', search_paths: @tools.cmdline_tools_search_paths)
99
94
  end
100
95
 
101
96
  # Add the `file-size` and `download-size` values of an APK to the helper, as reported by the corresponding `apkanalyzer apk …` commands
@@ -0,0 +1,48 @@
1
+ module Fastlane
2
+ module Actions
3
+ class AndroidShutdownEmulatorAction < Action
4
+ def self.run(params)
5
+ helper = Fastlane::Helper::Android::EmulatorHelper.new
6
+ helper.shut_down_emulators!(serials: params[:serials])
7
+ end
8
+
9
+ #####################################################
10
+ # @!group Documentation
11
+ #####################################################
12
+
13
+ def self.description
14
+ 'Shuts down Android emulators'
15
+ end
16
+
17
+ def self.details
18
+ description
19
+ end
20
+
21
+ def self.available_options
22
+ [
23
+ FastlaneCore::ConfigItem.new(key: :serials,
24
+ env_name: 'FL_ANDROID_SHUTDOWN_EMULATOR_SERIALS',
25
+ description: 'The serial(s) of the emulators to shut down. If not provided (nil), will shut them all down',
26
+ type: Array,
27
+ optional: true,
28
+ default_value: nil),
29
+ ]
30
+ end
31
+
32
+ def self.output
33
+ end
34
+
35
+ def self.return_value
36
+ # If you method provides a return value, you can describe here what it does
37
+ end
38
+
39
+ def self.authors
40
+ ['Automattic']
41
+ end
42
+
43
+ def self.is_supported?(platform)
44
+ platform == :android
45
+ end
46
+ end
47
+ end
48
+ 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
 
@@ -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
@@ -0,0 +1,198 @@
1
+ module Fastlane
2
+ module Helper
3
+ module Android
4
+ # Helper methods to manipulate System Images, AVDs and Android Emulators
5
+ #
6
+ class EmulatorHelper
7
+ BOOT_WAIT = 2
8
+ BOOT_TIMEOUT = 60
9
+
10
+ SHUTDOWN_WAIT = 2
11
+ SHUTDOWN_TIMEOUT = 60
12
+
13
+ def initialize
14
+ @tools = Fastlane::Helper::Android::ToolsPathHelper.new
15
+ end
16
+
17
+ # Installs the system-image suitable for a given Android `api`, with Google APIs, and for the current machine's architecture
18
+ #
19
+ # @param [Integer] api The Android API level to use
20
+ #
21
+ # @return [String] The `sdkmanager` package specifier that has been installed
22
+ #
23
+ def install_system_image(api:)
24
+ package = system_image_package(api: api)
25
+
26
+ UI.message("Installing System Image for Android #{api} (#{package})")
27
+ Actions.sh(@tools.sdkmanager, '--install', package)
28
+ UI.success("System Image #{package} successfully installed.")
29
+ package
30
+ end
31
+
32
+ # Create an emulator (AVD) for a given `api` number and `device` model
33
+ #
34
+ # @param [Integer] api The Android API level to use for this AVD
35
+ # @param [String] device The Device Model to use for this AVD. Valid values can be found using `avdmanager list devices`
36
+ # @param [String] name The name to give for the created AVD. Defaults to `<device>_API_<api>`.
37
+ # @param [String] sdcard The size of the SD card for this device. Defaults to `512M`.
38
+ #
39
+ # @return [String] The device name (i.e. either `name` if provided, or the derived `<device>_API_<api>` if provided `name` was `nil``)
40
+ #
41
+ def create_avd(api:, device:, system_image: nil, name: nil, sdcard: '512M')
42
+ package = system_image || system_image_package(api: api)
43
+ device_name = name || "#{device.gsub(' ', '_').capitalize}_API_#{api}"
44
+
45
+ UI.message("Creating AVD `#{device_name}` (#{device}, API #{api})")
46
+
47
+ Actions.sh(
48
+ @tools.avdmanager, 'create', 'avd',
49
+ '--force',
50
+ '--package', package,
51
+ '--device', device,
52
+ '--sdcard', sdcard,
53
+ '--name', device_name
54
+ )
55
+
56
+ UI.success("AVD `#{device_name}` successfully created.")
57
+
58
+ device_name
59
+ end
60
+
61
+ # Launch the emulator for the given AVD, then return the emulator serial
62
+ #
63
+ # @param [String] name name of the AVD to launch
64
+ # @param [Int] port the TCP port to use to connect to the emulator via adb. If nil (default), will let `emulator` pick the first free one.
65
+ # @param [Boolean] cold_boot if true, will do a cold boot, if false will try to use a previous snapshot of the device
66
+ # @param [Boolean] wipe_data if true, will wipe the emulator (i.e. reset the user data image)
67
+ #
68
+ # @return [String] emulator serial number corresponding to the launched AVD
69
+ #
70
+ def launch_avd(name:, port: nil, cold_boot: true, wipe_data: true)
71
+ UI.message("Launching emulator for #{name}")
72
+
73
+ params = ['-avd', name]
74
+ params << ['-port', port.to_s] unless port.nil?
75
+ params << '-no-snapshot' if cold_boot
76
+ params << '-wipe-data' if wipe_data
77
+
78
+ UI.command([@tools.emulator, *params].shelljoin)
79
+ # We want to launch emulator in the background to not block the rest of the code, so we can't use `Actions.sh` here
80
+ # We also want to filter the `stdout`+`stderr` emitted by the `emulator` process in the background,
81
+ # to limit verbosity and only print error lines, and also prefix those clearly (because they might happen
82
+ # at any moment in the background, so in parallel/the middle of other fastlane logs).
83
+ t = Thread.new do
84
+ Open3.popen2e(@tools.emulator, *params) do |i, oe, wait_thr|
85
+ i.close
86
+ until oe.eof?
87
+ line = oe.readline
88
+ UI.error("📱 [emulator]: #{line}") if line.start_with?(/ERROR|PANIC/)
89
+ next unless line.include?('PANIC: Broken AVD system path')
90
+
91
+ UI.user_error! <<~HINT
92
+ #{line}
93
+ Verify that your `sdkmanager/avdmanager` tools are not installed in a different SDK root than your `emulator` tool
94
+ (which can happen if you installed Android's command-line tools via `brew`, but the `emulator` via Android Studio, or vice-versa)
95
+ HINT
96
+ end
97
+ UI.error("📱 [emulator]: exited with non-zero status code: #{wait_thr.value.exitstatus}") unless wait_thr.value.success?
98
+ end
99
+ end
100
+ t.abort_on_exception = true # To bubble up any exception like `UI.user_error!` back to the main thread here
101
+
102
+ UI.message('Waiting for emulator to start...')
103
+ # Loop until the emulator has started and shows up in `adb devices -l` so we can find its serial
104
+ serial = nil
105
+ retry_loop(time_between_retries: BOOT_WAIT, timeout: BOOT_TIMEOUT, description: 'waiting for emulator to start') do
106
+ serial = find_serial(avd_name: name)
107
+ !serial.nil?
108
+ end
109
+ UI.message("Found device `#{name}` with serial `#{serial}`")
110
+
111
+ # Once the emulator has started, wait for the device in the emulator to finish booting
112
+ UI.message('Waiting for device to finish booting...')
113
+ retry_loop(time_between_retries: BOOT_WAIT, timeout: BOOT_TIMEOUT, description: 'waiting for device to finish booting') do
114
+ Actions.sh(@tools.adb, '-s', serial, 'shell', 'getprop', 'sys.boot_completed').chomp == '1'
115
+ end
116
+
117
+ UI.success("Emulator #{name} successfully booted as `#{serial}`.")
118
+
119
+ serial
120
+ end
121
+
122
+ # @return [Array<Fastlane::Helper::AdbDevice>] List of currently booted emulators
123
+ #
124
+ def running_emulators
125
+ helper = Fastlane::Helper::AdbHelper.new(adb_path: @tools.adb)
126
+ helper.load_all_devices.select { |device| device.serial.include?('emulator') }
127
+ end
128
+
129
+ def find_serial(avd_name:)
130
+ running_emulators.find do |candidate|
131
+ command = [@tools.adb, '-s', candidate.serial, 'emu', 'avd', 'name']
132
+ UI.command(command.shelljoin)
133
+ candidate_name = Actions.sh(*command, log: false).split("\n").first.chomp
134
+ candidate_name == avd_name
135
+ end&.serial
136
+ end
137
+
138
+ # Trigger a shutdown for all running emulators, and wait until there is no more emulators running.
139
+ #
140
+ # @param [Array<String>] serials List of emulator serials to shut down. Will shut down all of them if `nil`.
141
+ #
142
+ def shut_down_emulators!(serials: nil)
143
+ UI.message("Shutting down #{serials || 'all'} emulator(s)...")
144
+
145
+ emulators_list = running_emulators.map(&:serial)
146
+ # Get the intersection of the set of running emulators with the ones we want to shut down
147
+ emulators_list &= serials unless serials.nil?
148
+ emulators_list.each do |e|
149
+ Actions.sh(@tools.adb, '-s', e, 'emu', 'kill') { |_| } # ignore error if no emulator with specified serial is running
150
+
151
+ # NOTE: Alternative way of shutting down emulator would be to call the following command instead, which shuts down the emulator more gracefully:
152
+ # `adb -s #{e} shell reboot -p` # In case you're wondering, `-p` is for "power-off"
153
+ # But this alternate command:
154
+ # - Requires that `-no-snapshot` was used on boot (to avoid being prompted to save current state on shutdown)
155
+ # - Disconnects the emulator from `adb` (and thus disappear from `adb devices -l`) for a short amount of time,
156
+ # before reconnecting to it but in an `offline` state, until `emulator` finally completely quits and it disappears
157
+ # again (for good) from `adb devices --list`.
158
+ # This means that so if we used alternative, we couldn't really retry_loop until emulator disappears from `running_emulators` to detect
159
+ # that the shutdown was really complete, as we might as well accidentally detect the intermediate disconnect instead.
160
+ end
161
+
162
+ # Wait until all emulators are killed
163
+ retry_loop(time_between_retries: SHUTDOWN_WAIT, timeout: SHUTDOWN_TIMEOUT, description: 'waiting for devices to shutdown') do
164
+ (emulators_list & running_emulators.map(&:serial)).empty?
165
+ end
166
+
167
+ UI.success('All emulators are now shut down.')
168
+ end
169
+
170
+ # Find the system-images package for the provided `api`, with Google APIs, and matching the current platform/architecture this lane is called from.
171
+ #
172
+ # @param [Integer] api The Android API level to use for this AVD
173
+ # @return [String] The `system-images;android-<N>;google_apis;<platform>` package specifier for `sdkmanager` to use in its install command
174
+ #
175
+ # @note Results from this method are memoized, to avoid repeating calls to `sdkmanager` when querying for the same api level multiple times.
176
+ #
177
+ def system_image_package(api:)
178
+ @system_image_packages ||= {}
179
+ @system_image_packages[api] ||= begin
180
+ platform = `uname -m`.chomp
181
+ all_packages = `#{@tools.sdkmanager} --list`
182
+ package = all_packages.match(/^ *(system-images;android-#{api};google_apis;#{platform}(-[^ ]*)?)/)&.captures&.first
183
+ UI.user_error!("Could not find system-image for API `#{api}` and your platform `#{platform}` in `sdkmanager --list`. Maybe Google removed it for download and it's time to update to a newer API?") if package.nil?
184
+ package
185
+ end
186
+ end
187
+
188
+ def retry_loop(time_between_retries:, timeout:, description:)
189
+ Timeout.timeout(timeout) do
190
+ sleep(time_between_retries) until yield
191
+ end
192
+ rescue Timeout::Error
193
+ UI.user_error!("Timed out #{description}")
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,87 @@
1
+ require 'fastlane_core/ui/ui'
2
+
3
+ module Fastlane
4
+ module Helper
5
+ module Android
6
+ # Helper to find the paths of common Android build and SDK tools on the current machine
7
+ # Based on `$ANDROID_HOME` and the common relative paths those tools are installed in.
8
+ #
9
+ class ToolsPathHelper
10
+ attr_reader :android_home
11
+
12
+ def initialize(sdk_root: nil)
13
+ @android_home = sdk_root || ENV['ANDROID_HOME'] || ENV['ANDROID_SDK_ROOT'] || ENV['ANDROID_SDK']
14
+ end
15
+
16
+ # @param [String] binary The name of the binary to search for
17
+ # @param [Array<String>] search_paths The search paths, relative to `@android_home`, in which to search for the tools.
18
+ # If `android_home` is `nil` or the binary wasn't found in any of the `search_paths`, will fallback to searching in `$PATH`.
19
+ # @return [String] The absolute path of the tool if found, `nil` if not found.
20
+ def find_tool_path(binary:, search_paths:)
21
+ bin_path = unless android_home.nil?
22
+ search_paths
23
+ .map { |path| File.join(android_home, path, binary) }
24
+ .find { |path| File.executable?(path) }
25
+ end
26
+
27
+ # If not found in any of the `search_paths`, try to look for it in $PATH
28
+ bin_path ||= Actions.sh('command', '-v', binary) { |err, res, _| res if err&.success? }&.chomp
29
+
30
+ # Normalize return value to `nil` if it was not found, empty, or is not an executable
31
+ bin_path = nil if !bin_path.nil? && (bin_path.empty? || !File.executable?(bin_path))
32
+
33
+ bin_path
34
+ end
35
+
36
+ # @param [String] binary The name of the binary to search for
37
+ # @param [Array<String>] search_paths The search paths, relative to `@android_home`, in which to search for the tools.
38
+ # If `android_home` is `nil` or the binary wasn't found in any of the `search_paths`, will fallback to searching in `$PATH`.
39
+ # @return [String] The absolute path of the tool if found.
40
+ # @raise [FastlaneCore::Interface::FastlaneError] If the tool couldn't be found.
41
+ def find_tool_path!(binary:, search_paths:)
42
+ bin_path = find_tool_path(binary: binary, search_paths: search_paths)
43
+ UI.user_error!("Unable to find path for #{binary} in #{search_paths.inspect}. Verify you installed the proper Android tools.") if bin_path.nil?
44
+ bin_path
45
+ end
46
+
47
+ def cmdline_tools_search_paths
48
+ # It appears that depending on the machines and versions of Android SDK, some versions
49
+ # installed the command line tools in `tools` and not `latest` subdirectory, hence why
50
+ # we search both (`latest` first, `tools` as fallback) to cover all our bases.
51
+ [
52
+ File.join('cmdline-tools', 'latest', 'bin'),
53
+ File.join('cmdline-tools', 'tools', 'bin'),
54
+ ]
55
+ end
56
+
57
+ def sdkmanager
58
+ @sdkmanager ||= find_tool_path!(
59
+ binary: 'sdkmanager',
60
+ search_paths: cmdline_tools_search_paths
61
+ )
62
+ end
63
+
64
+ def avdmanager
65
+ @avdmanager ||= find_tool_path!(
66
+ binary: 'avdmanager',
67
+ search_paths: cmdline_tools_search_paths
68
+ )
69
+ end
70
+
71
+ def emulator
72
+ @emulator ||= find_tool_path!(
73
+ binary: 'emulator',
74
+ search_paths: [File.join('emulator')]
75
+ )
76
+ end
77
+
78
+ def adb
79
+ @adb ||= find_tool_path!(
80
+ binary: 'adb',
81
+ search_paths: [File.join('platform-tools')]
82
+ )
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -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.
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module Wpmreleasetoolkit
3
- VERSION = '5.5.0'
3
+ VERSION = '6.0.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.5.0
4
+ version: 6.0.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-15 00:00:00.000000000 Z
11
+ date: 2022-11-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -260,14 +260,14 @@ dependencies:
260
260
  requirements:
261
261
  - - "~>"
262
262
  - !ruby/object:Gem::Version
263
- version: '2'
263
+ version: '2.210'
264
264
  type: :development
265
265
  prerelease: false
266
266
  version_requirements: !ruby/object:Gem::Requirement
267
267
  requirements:
268
268
  - - "~>"
269
269
  - !ruby/object:Gem::Version
270
- version: '2'
270
+ version: '2.210'
271
271
  - !ruby/object:Gem::Dependency
272
272
  name: pry
273
273
  requirement: !ruby/object:Gem::Requirement
@@ -402,6 +402,7 @@ files:
402
402
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_bump_version_release.rb
403
403
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_codefreeze_prechecks.rb
404
404
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_completecodefreeze_prechecks.rb
405
+ - lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_create_avd_action.rb
405
406
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_create_xml_release_notes.rb
406
407
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_current_branch_is_hotfix.rb
407
408
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_download_file_by_version.rb
@@ -412,7 +413,9 @@ files:
412
413
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_app_version.rb
413
414
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_get_release_version.rb
414
415
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_hotfix_prechecks.rb
416
+ - lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_launch_emulator_action.rb
415
417
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb
418
+ - lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_shutdown_emulator_action.rb
416
419
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_tag_build.rb
417
420
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_update_release_notes.rb
418
421
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/common/buildkite_trigger_build_action.rb
@@ -471,8 +474,10 @@ files:
471
474
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_update_release_notes.rb
472
475
  - lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_validate_ci_build.rb
473
476
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/an_metadata_update_helper.rb
477
+ - lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_emulator_helper.rb
474
478
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_git_helper.rb
475
479
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb
480
+ - lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_tools_path_helper.rb
476
481
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_version_helper.rb
477
482
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb
478
483
  - lib/fastlane/plugin/wpmreleasetoolkit/helper/ci_helper.rb