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.
- checksums.yaml +4 -4
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_create_avd_action.rb +90 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_download_file_by_version.rb +3 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_launch_emulator_action.rb +61 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb +6 -11
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_shutdown_emulator_action.rb +48 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/close_milestone_action.rb +5 -2
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/comment_on_pr.rb +3 -7
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_new_milestone_action.rb +17 -2
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/create_release_action.rb +3 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/get_prs_list_action.rb +3 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/removebranchprotection_action.rb +12 -6
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/setbranchprotection_action.rb +12 -5
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/setfrozentag_action.rb +5 -2
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/common/upload_to_s3.rb +16 -1
- data/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_lint_localizations.rb +17 -15
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_emulator_helper.rb +198 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_tools_path_helper.rb +87 -0
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/github_helper.rb +110 -46
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb +24 -4
- data/lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_strings_file_validation_helper.rb +14 -11
- data/lib/fastlane/plugin/wpmreleasetoolkit/version.rb +1 -1
- metadata +9 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c16be217cbd668e4fbd9da107edcdba4b5be149f0d64d4dadb6a0d5697cde006
|
4
|
+
data.tar.gz: e736842ca7df0e1ff50fc0a7114e39555570c03c8e58b41fa265999f267f0ec2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_download_file_by_version.rb
CHANGED
@@ -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.
|
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 `$
|
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
|
-
|
81
|
-
|
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 `$
|
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
|
-
|
97
|
-
|
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
|
data/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_shutdown_emulator_action.rb
ADDED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
Fastlane::Helper::GithubHelper.
|
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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
5
|
+
violations = nil
|
6
6
|
|
7
7
|
loop do
|
8
|
-
|
9
|
-
#
|
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.
|
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
|
-
|
63
|
-
|
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
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
31
|
-
|
20
|
+
# Fetch the current user
|
21
|
+
user = @client.user
|
22
|
+
UI.message("Logged in as: #{user.name}")
|
32
23
|
|
33
|
-
|
34
|
-
|
24
|
+
# Auto-paginate to ensure we're not missing data
|
25
|
+
@client.auto_paginate = true
|
35
26
|
end
|
36
27
|
|
37
|
-
def
|
38
|
-
miles =
|
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
|
55
|
-
|
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
|
49
|
+
def get_last_milestone(repository)
|
59
50
|
options = {}
|
60
51
|
options[:state] = 'open'
|
61
52
|
|
62
|
-
milestones =
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
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
|
-
|
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
|
110
|
-
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
|
-
|
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
|
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 =
|
139
|
-
|
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
|
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
|
72
|
-
|
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
|
-
/./
|
28
|
+
/./mu => :in_block_comment
|
29
29
|
},
|
30
30
|
maybe_block_comment_end: {
|
31
31
|
'/' => :root,
|
32
|
-
/./
|
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
|
-
/./
|
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
|
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.
|
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:
|
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-
|
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
|