fastlane-plugin-wpmreleasetoolkit 5.5.0 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
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