fastlane-plugin-maestro_orchestration 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +148 -0
- data/lib/fastlane/plugin/maestro_orchestration/actions/maestro_orchestration_android_action.rb +197 -0
- data/lib/fastlane/plugin/maestro_orchestration/actions/maestro_orchestration_api_request_action.rb +144 -0
- data/lib/fastlane/plugin/maestro_orchestration/actions/maestro_orchestration_ios_action.rb +166 -0
- data/lib/fastlane/plugin/maestro_orchestration/actions/maestro_orchestration_s3_upload_action.rb +106 -0
- data/lib/fastlane/plugin/maestro_orchestration/helper/maestro_orchestration_helper.rb +132 -0
- data/lib/fastlane/plugin/maestro_orchestration/version.rb +5 -0
- data/lib/fastlane/plugin/maestro_orchestration.rb +16 -0
- metadata +72 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 16bca8a824864e18e5022ff562fb194076093be36b157665c8a9b35badbe047e
|
4
|
+
data.tar.gz: 32fe2d534c4d295245009d6672750f748adfdfce4dcd6ca8864a60e989d19936
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8f5c55f53f32f13711091ebb0e989b7475b133e64739ff839966a62239eef3fd8a53d555ee1c64da888c1ba810bed487b9316f61786f56cc4d8564fdcfe03a5c
|
7
|
+
data.tar.gz: e923fa4cae04e21f78111f8d1dc0083b88a616f3c60a5d1570750087349014608eff5926dec3cc9d9cb734fa7963b13e46cbec082ff4022149e20f4a373bdb0b
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Nemanja Risteski <nemanja.risteski@sourcetoad.com>
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
# maestro_orchestration plugin
|
2
|
+
|
3
|
+
[](https://rubygems.org/gems/fastlane-plugin-maestro_orchestration)
|
4
|
+
|
5
|
+
## Getting Started
|
6
|
+
|
7
|
+
This project is a [_fastlane_](https://github.com/fastlane/fastlane) plugin. To get started with `fastlane-plugin-maestro_orchestration`, add it to your project by running:
|
8
|
+
|
9
|
+
```bash
|
10
|
+
fastlane add_plugin maestro_orchestration
|
11
|
+
```
|
12
|
+
|
13
|
+
## About maestro_orchestration
|
14
|
+
|
15
|
+
The `maestro_orchestration` plugin enhances your Fastlane workflows by integrating with the Maestro testing framework. It provides the following actions:
|
16
|
+
|
17
|
+
### 1. `maestro_orchestration` - separate actions for iOS and Android platform.
|
18
|
+
Executes Maestro test suites within your Fastlane lanes, facilitating automated UI testing for mobile applications.
|
19
|
+
|
20
|
+
## Parameters `iOS`
|
21
|
+
|
22
|
+
| Parameter | Env Name | Notes |
|
23
|
+
| ------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
24
|
+
| `workspace` | `MAESTRO_IOS_WORKSPACE` | Path to the project's Xcode workspace directory. <br> **Required** |
|
25
|
+
| `scheme` | `MAESTRO_IOS_SCHEME` | The iOS app scheme to build. <br> **Required** |
|
26
|
+
| `maestro_flow_file` | `MAESTRO_IOS_FLOW_FILE` | The path to the Maestro flows YAML file. <br> **Required** |
|
27
|
+
| `simulator_name` | `MAESTRO_IOS_DEVICE_NAME` | The iOS simulator device to boot. <br> **Default value:** 'iPhone 15' |
|
28
|
+
| `device_type` | `MAESTRO_IOS_DEVICE` | The iOS simulator device type for new simulator (e.g., iPhone #, iPad, etc...). <br> **Default value:** 'com.apple.CoreSimulator.SimDeviceType.iPhone-15'|
|
29
|
+
|
30
|
+
## Parameters `Android`
|
31
|
+
|
32
|
+
| Parameter | Env Name | Notes |
|
33
|
+
| ------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
34
|
+
| `sdk_dir` | `MAESTRO_ANDROID_SDK_DIR` | Path to the Android SDK DIR. <br> **Required** `ENV["ANDROID_HOME"]`, `ENV["ANDROID_SDK_ROOT"]`, `~/Library/Android/sdk` |
|
35
|
+
| `maestro_flow_file` | `MAESTRO_IOS_FLOW_FILE` | The path to the Maestro flows YAML file. <br> **Required** |
|
36
|
+
| `emulator_name` | `MAESTRO_AVD_NAME` | Name of the AVD. <br> **Default value:** 'Maestro\_Android\_Emulator' |
|
37
|
+
| `emulator_package` | `MAESTRO_AVD_PACKAGE` | The selected system image of the emulator. <br> **Default value:** 'system-images;android-35;google_apis_playstore;arm64-v8a' |
|
38
|
+
| `emulator_device` | `MAESTRO_AVD_DEVICE` | Type of android device. <br> **Default value:** 'pixel_7_pro' |
|
39
|
+
| `emulator_port` | `MAESTRO_AVD_PORT` | Port of the emulator. <br> **Default value:** 5554 |
|
40
|
+
|
41
|
+
### 2. `maestro_orchestartion_s3_upload`
|
42
|
+
Uploads a folder of files (such as screenshots) to an S3 bucket, organizing them based on the app version, theme, and device type.
|
43
|
+
|
44
|
+
| Parameter | Env Name | Notes |
|
45
|
+
| ------------- | -------- | --------------------------------------------------------------------------------------------------------------- |
|
46
|
+
| `folder_path` | `MAESTRO_SCREENSHOTS_FOLDER_PATH` | Path to the local folder containing the files to upload. <br> **Required** |
|
47
|
+
| `bucket` | `MAESTRO_SCREENSHOTS_S3_BUCKET` | The name of the S3 bucket where files will be uploaded. <br> **Required** |
|
48
|
+
| `s3_path` | `MAESTRO_SCREENSHOTS_S3_PATH` | The base S3 path (excluding the bucket name). <br> **Required** |
|
49
|
+
| `version` | `MAESTRO_SCREENSHOTS_APP_VERSION` | The app version associated with the uploaded files. <br> **Required** |
|
50
|
+
| `device` | `MAESTRO_SCREENSHOTS_DEVICE` | The target device type (android or ios). <br> **Required** |
|
51
|
+
| `theme` | `MAESTRO_SCREENSHOTS_APPLICATION_THEME` | The application theme (e.g., dark or light). <br> Optional |
|
52
|
+
|
53
|
+
### 3. `maestro_orchestration_api_request`
|
54
|
+
Sends an API request with a signed payload, typically used to notify external systems of events such as the completion of test runs or the availability of new screenshots.
|
55
|
+
|
56
|
+
| Parameter | Env Name | Notes |
|
57
|
+
| ------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
58
|
+
| `s3_path` | `MAESTRO_SCREENSHOTS_S3_PATH` | The base S3 path (excluding the bucket name) where files are uploaded. <br> **Required** |
|
59
|
+
| `version` | `MAESTRO_SCREENSHOTS_APP_VERSION` | The version of the app associated with the screenshots or test results. <br> **Required** |
|
60
|
+
| `device` | `MAESTRO_SCREENSHOTS_APP_VERSION` | The device type (android or ios). <br> **Required** |
|
61
|
+
| `theme` | `MAESTRO_SCREENSHOTS_APPLICATION_THEME` | The application theme (e.g., dark or light). <br> Optional |
|
62
|
+
| `hmac_secret` | `MAESTRO_SCREENSHOTS_HMAC_SECRET` | The HMAC secret used to sign the payload for security purposes. <br> **Required** |
|
63
|
+
| `url` | `MAESTRO_SCREENSHOTS_WEBHOOK_URL` | The endpoint URL to which the API request is sent. <br> **Required** |
|
64
|
+
|
65
|
+
## Example
|
66
|
+
|
67
|
+
**iOS**
|
68
|
+
```ruby
|
69
|
+
lane :maestro do |options|
|
70
|
+
maestro_orchestration_ios(
|
71
|
+
scheme: your_app,
|
72
|
+
workspace: your_app.xcworskapce,
|
73
|
+
maestro_flow_file: "../.maestro/flow_ios.yaml"
|
74
|
+
)
|
75
|
+
|
76
|
+
maestro_orchestration_s3_upload(
|
77
|
+
folder_path: "../.maestro/android/screenshots,
|
78
|
+
bucket: "your-s3-bucket-name",
|
79
|
+
s3_path: "path/to/s3/folder",
|
80
|
+
version: "1.0.0",
|
81
|
+
device: "android",
|
82
|
+
theme: "dark" # optional
|
83
|
+
)
|
84
|
+
|
85
|
+
maestro_orchestration_api_request(
|
86
|
+
s3_path: "path/to/s3/folder",
|
87
|
+
version: "1.0.0",
|
88
|
+
device: "android",
|
89
|
+
hmac_secret: "your-hmac-secret",
|
90
|
+
url: "https://your-webhook-url.com"
|
91
|
+
)
|
92
|
+
end
|
93
|
+
```
|
94
|
+
**Android**
|
95
|
+
```ruby
|
96
|
+
lane :maestro do |options|
|
97
|
+
maestro_orchestration_android(
|
98
|
+
maestro_flow_file: "../.maestro/flow_android.yaml
|
99
|
+
)
|
100
|
+
|
101
|
+
maestro_orchestration_s3_upload(
|
102
|
+
folder_path: "../.maestro/android/screenshots,
|
103
|
+
bucket: "your-s3-bucket-name",
|
104
|
+
s3_path: "path/to/s3/folder",
|
105
|
+
version: "1.0.0",
|
106
|
+
device: "android",
|
107
|
+
theme: "dark" # optional
|
108
|
+
)
|
109
|
+
|
110
|
+
maestro_orchestration_api_request(
|
111
|
+
s3_path: "path/to/s3/folder",
|
112
|
+
version: "1.0.0",
|
113
|
+
device: "android",
|
114
|
+
hmac_secret: "your-hmac-secret",
|
115
|
+
url: "https://your-webhook-url.com"
|
116
|
+
)
|
117
|
+
end
|
118
|
+
```
|
119
|
+
**Note:** For Android platform, the plugin relies on the already previously generated build by Fastlane instead of generating a new one like for the iOS. The plugin was intended to run on simulators, and iOS has differents build types for simulators and real devices.
|
120
|
+
|
121
|
+
## Run tests for this plugin
|
122
|
+
|
123
|
+
To run both the tests, and code style validation, run
|
124
|
+
|
125
|
+
```
|
126
|
+
rake
|
127
|
+
```
|
128
|
+
|
129
|
+
To automatically fix many of the styling issues, use
|
130
|
+
```
|
131
|
+
rubocop -a
|
132
|
+
```
|
133
|
+
|
134
|
+
## Issues and Feedback
|
135
|
+
|
136
|
+
For any other issues and feedback about this plugin, please submit it to this repository.
|
137
|
+
|
138
|
+
## Troubleshooting
|
139
|
+
|
140
|
+
If you have trouble using plugins, check out the [Plugins Troubleshooting](https://docs.fastlane.tools/plugins/plugins-troubleshooting/) guide.
|
141
|
+
|
142
|
+
## Using _fastlane_ Plugins
|
143
|
+
|
144
|
+
For more information about how the `fastlane` plugin system works, check out the [Plugins documentation](https://docs.fastlane.tools/plugins/create-plugin/).
|
145
|
+
|
146
|
+
## About _fastlane_
|
147
|
+
|
148
|
+
_fastlane_ is the easiest way to automate beta deployments and releases for your iOS and Android apps. To learn more, check out [fastlane.tools](https://fastlane.tools).
|
data/lib/fastlane/plugin/maestro_orchestration/actions/maestro_orchestration_android_action.rb
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
require 'fastlane/action'
|
2
|
+
require 'fastlane_core/configuration/config_item'
|
3
|
+
require 'fastlane/helper/adb_helper'
|
4
|
+
require_relative '../helper/maestro_orchestration_helper'
|
5
|
+
|
6
|
+
module Fastlane
|
7
|
+
module Actions
|
8
|
+
class MaestroOrchestrationAndroidAction < Action
|
9
|
+
def self.run(params)
|
10
|
+
required_params = [:emulator_package, :emulator_device, :maestro_flow_file]
|
11
|
+
missing_params = required_params.select { |param| params[param].nil? }
|
12
|
+
|
13
|
+
if missing_params.any?
|
14
|
+
missing_params.each do |param|
|
15
|
+
UI.error("Missing parameter: #{param}")
|
16
|
+
end
|
17
|
+
raise "Missing required parameters: #{missing_params.join(', ')}"
|
18
|
+
end
|
19
|
+
|
20
|
+
UI.message("--------------\n\nSDK DIR: #{params[:sdk_dir]}\n\n--------------")
|
21
|
+
adb = Helper::AdbHelper.new
|
22
|
+
|
23
|
+
setup_emulator(params)
|
24
|
+
sleep(5)
|
25
|
+
demo_mode(params)
|
26
|
+
install_android_app(params)
|
27
|
+
|
28
|
+
UI.message("Running Maestro tests on Android...")
|
29
|
+
devices = adb.load_all_devices
|
30
|
+
if devices.empty?
|
31
|
+
UI.message("No running emulators found.")
|
32
|
+
else
|
33
|
+
sleep(2)
|
34
|
+
UI.message("Devices: #{devices}")
|
35
|
+
devices.each do |device|
|
36
|
+
sh("maestro --device #{device.serial} test #{params[:maestro_flow_file]}")
|
37
|
+
UI.success("Finished Maestro tests on Android.")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
UI.message("Exit demo mode and kill Android emulator...")
|
42
|
+
adb.trigger(command: "shell am broadcast -a com.android.systemui.demo -e command exit", serial: devices.first.serial)
|
43
|
+
sleep(5)
|
44
|
+
adb.trigger(command: "emu kill", serial: devices.first.serial)
|
45
|
+
UI.success("Android emulator killed. Process finished.")
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.setup_emulator(params)
|
49
|
+
emulator = Helper::EmulatorHelper.new
|
50
|
+
adb = Helper::AdbHelper.new
|
51
|
+
avdmanager = Helper::AvdHelper.new
|
52
|
+
|
53
|
+
UI.message("Stop all running emulators...")
|
54
|
+
devices = adb.load_all_devices
|
55
|
+
UI.success("Devices: #{devices}")
|
56
|
+
|
57
|
+
if devices.empty?
|
58
|
+
UI.message("No running emulators found.")
|
59
|
+
else
|
60
|
+
devices.each do |device|
|
61
|
+
UI.message("Stopping emulator: #{device.serial}")
|
62
|
+
adb.trigger(command: "emu kill", serial: device.serial)
|
63
|
+
sleep(10)
|
64
|
+
system("Stopped emulator: #{device.serial}")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
UI.message("Waiting for all emulators to stop...")
|
68
|
+
sleep(10)
|
69
|
+
|
70
|
+
UI.message("Setting up new Android emulator...")
|
71
|
+
avdmanager.create_avd(name: params[:emulator_name], package: params[:emulator_package], device: params[:emulator_device])
|
72
|
+
|
73
|
+
UI.message("Starting Android emulator...")
|
74
|
+
emulator.start_emulator(name: params[:emulator_name], port: params[:emulator_port])
|
75
|
+
adb.trigger(command: "wait-for-device", serial: "emulator-#{params[:emulator_port]}")
|
76
|
+
|
77
|
+
max_retries = 10
|
78
|
+
booted = Helper::MaestroOrchestrationHelper.wait_for_emulator_to_boot(adb, max_retries, "emulator-#{params[:emulator_port]}")
|
79
|
+
|
80
|
+
unless booted
|
81
|
+
UI.error("Emulator failed to boot after #{max_retries} attempts. Restarting ADB server...")
|
82
|
+
adb.trigger(command: "kill-server")
|
83
|
+
adb.trigger(command: "start-server")
|
84
|
+
UI.message("ADB server restarted. Retrying boot process...")
|
85
|
+
|
86
|
+
# Retry boot process after restarting ADB server
|
87
|
+
booted = Helper::MaestroOrchestrationHelper.wait_for_emulator_to_boot(adb, max_retries, "emulator-#{params[:emulator_port]}")
|
88
|
+
end
|
89
|
+
|
90
|
+
if booted
|
91
|
+
UI.success("Emulator is online and fully booted!")
|
92
|
+
else
|
93
|
+
UI.error("Emulator failed to boot even after restarting ADB server.")
|
94
|
+
raise "Failed to boot emulator. Please check emulator logs and configuration."
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.demo_mode(params)
|
99
|
+
adb = Helper::AdbHelper.new
|
100
|
+
adb.load_all_devices
|
101
|
+
serial = adb.devices.first.serial
|
102
|
+
|
103
|
+
UI.message("Checking and allowing demo mode on Android emulator...")
|
104
|
+
adb.trigger(command: "shell settings put global sysui_demo_allowed 1", serial: serial)
|
105
|
+
adb.trigger(command: "shell settings get global sysui_demo_allowed", serial: serial)
|
106
|
+
|
107
|
+
UI.message("Setting demo mode commands...")
|
108
|
+
adb.trigger(command: "shell am broadcast -a com.android.systemui.demo -e command enter", serial: serial)
|
109
|
+
adb.trigger(command: "shell am broadcast -a com.android.systemui.demo -e command clock -e hhmm 1200", serial: serial)
|
110
|
+
adb.trigger(command: "shell am broadcast -a com.android.systemui.demo -e command battery -e level 100", serial: serial)
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.install_android_app(params)
|
114
|
+
UI.message("Installing Android app...")
|
115
|
+
|
116
|
+
adb = Helper::AdbHelper.new
|
117
|
+
adb.load_all_devices
|
118
|
+
serial = adb.devices.first.serial
|
119
|
+
|
120
|
+
apk_path = Dir["app/build/outputs/apk/release/*.apk"].first
|
121
|
+
|
122
|
+
if apk_path.nil?
|
123
|
+
UI.user_error!("Error: APK file not found in build outputs.")
|
124
|
+
end
|
125
|
+
|
126
|
+
UI.message("Found APK file at: #{apk_path}")
|
127
|
+
adb.trigger(command: "install -r '#{apk_path}'", serial: serial)
|
128
|
+
UI.success("APK installed on Android emulator.")
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.description
|
132
|
+
"Boots an Android emulator, builds the app, installs it, and runs Maestro tests"
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.available_options
|
136
|
+
[
|
137
|
+
FastlaneCore::ConfigItem.new(
|
138
|
+
key: :sdk_dir,
|
139
|
+
env_name: "MAESTRO_ANDROID_SDK_DIR",
|
140
|
+
description: "Path to the Android SDK DIR",
|
141
|
+
default_value: ENV["ANDROID_HOME"] || ENV["ANDROID_SDK_ROOT"] || "~/Library/Android/sdk",
|
142
|
+
optional: true,
|
143
|
+
verify_block: proc do |value|
|
144
|
+
UI.user_error!("No ANDROID_SDK_DIR given, pass using `sdk_dir: 'sdk_dir'`") unless value && !value.empty?
|
145
|
+
end
|
146
|
+
),
|
147
|
+
FastlaneCore::ConfigItem.new(
|
148
|
+
key: :emulator_name,
|
149
|
+
env_name: "MAESTRO_AVD_NAME",
|
150
|
+
description: "Name of the AVD",
|
151
|
+
default_value: "Maestro_Android_Emulator",
|
152
|
+
optional: true
|
153
|
+
),
|
154
|
+
FastlaneCore::ConfigItem.new(
|
155
|
+
key: :emulator_package,
|
156
|
+
env_name: "MAESTRO_AVD_PACKAGE",
|
157
|
+
description: "The selected system image of the emulator",
|
158
|
+
default_value: "system-images;android-35;google_apis_playstore;arm64-v8a",
|
159
|
+
optional: true
|
160
|
+
),
|
161
|
+
FastlaneCore::ConfigItem.new(
|
162
|
+
key: :emulator_device,
|
163
|
+
env_name: "MAESTRO_AVD_DEVICE",
|
164
|
+
description: "Device",
|
165
|
+
default_value: "pixel_7_pro",
|
166
|
+
optional: true
|
167
|
+
),
|
168
|
+
FastlaneCore::ConfigItem.new(
|
169
|
+
key: :location,
|
170
|
+
env_name: "MAESTRO_AVD_LOCATION",
|
171
|
+
description: "Set location of the emulator '<longitude> <latitude>'",
|
172
|
+
default_value: "28.0362979, -82.4930012",
|
173
|
+
optional: true
|
174
|
+
),
|
175
|
+
FastlaneCore::ConfigItem.new(
|
176
|
+
key: :emulator_port,
|
177
|
+
env_name: "MAESTRO_AVD_PORT",
|
178
|
+
description: "Port of the emulator",
|
179
|
+
default_value: "5554",
|
180
|
+
optional: true
|
181
|
+
),
|
182
|
+
FastlaneCore::ConfigItem.new(
|
183
|
+
key: :maestro_flow_file,
|
184
|
+
env_name: "MAESTRO_ANDROID_FLOW_FILE",
|
185
|
+
description: "The path to the Maestro flow YAML file",
|
186
|
+
optional: false,
|
187
|
+
type: String
|
188
|
+
)
|
189
|
+
]
|
190
|
+
end
|
191
|
+
|
192
|
+
def self.is_supported?(platform)
|
193
|
+
platform == :android
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
data/lib/fastlane/plugin/maestro_orchestration/actions/maestro_orchestration_api_request_action.rb
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'fastlane/action'
|
2
|
+
require 'openssl'
|
3
|
+
require 'net/http'
|
4
|
+
require 'uri'
|
5
|
+
require 'json'
|
6
|
+
require 'fastlane_core/configuration/config_item'
|
7
|
+
require_relative '../helper/maestro_orchestration_helper'
|
8
|
+
|
9
|
+
module Fastlane
|
10
|
+
module Actions
|
11
|
+
class MaestroOrchestrationApiRequestAction < Action
|
12
|
+
def self.run(params)
|
13
|
+
required_params = [:s3_path, :version, :device, :hmac_secret, :url]
|
14
|
+
missing_params = required_params.select { |param| params[param].nil? }
|
15
|
+
|
16
|
+
if missing_params.any?
|
17
|
+
missing_params.each do |param|
|
18
|
+
UI.error("Missing parameter: #{param}")
|
19
|
+
end
|
20
|
+
raise "Missing required parameters: #{missing_params.join(', ')}"
|
21
|
+
end
|
22
|
+
|
23
|
+
base_path = "#{params[:s3_path]}/ver:#{params[:version]}"
|
24
|
+
base_path += "/theme:#{params[:theme]}" if params[:theme]
|
25
|
+
base_path += "/device:#{params[:device]}"
|
26
|
+
|
27
|
+
payload = {
|
28
|
+
message: "Screenshots uploaded",
|
29
|
+
version: params[:version],
|
30
|
+
folder_path: base_path
|
31
|
+
}
|
32
|
+
payload_json = payload.to_json
|
33
|
+
|
34
|
+
signature = "sha256=#{OpenSSL::HMAC.hexdigest('SHA256', params[:hmac_secret], payload_json)}"
|
35
|
+
|
36
|
+
uri = URI.parse(params[:url])
|
37
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
38
|
+
http.use_ssl = (uri.scheme == "https")
|
39
|
+
|
40
|
+
request = Net::HTTP::Post.new(uri.path, {
|
41
|
+
"Content-Type" => "application/json",
|
42
|
+
"X-Action-Signature" => signature
|
43
|
+
})
|
44
|
+
request.body = payload_json
|
45
|
+
|
46
|
+
response = http.request(request)
|
47
|
+
|
48
|
+
if response.kind_of?(Net::HTTPSuccess)
|
49
|
+
UI.success("API request successful: #{response.code} - #{response.body}")
|
50
|
+
else
|
51
|
+
UI.user_error!("API request failed: #{response.code} - #{response.body}")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.description
|
56
|
+
"Sends an API request with a signed payload."
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.available_options
|
60
|
+
[
|
61
|
+
s3_path_option,
|
62
|
+
version_option,
|
63
|
+
device_option,
|
64
|
+
theme_option,
|
65
|
+
hmac_secret_option,
|
66
|
+
url_option
|
67
|
+
]
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.s3_path_option
|
71
|
+
FastlaneCore::ConfigItem.new(
|
72
|
+
key: :s3_path,
|
73
|
+
env_name: "MAESTRO_SCREENSHOTS_S3_PATH",
|
74
|
+
description: "The base S3 path (after the bucket name) where files will be uploaded: $bucket/$s3_path",
|
75
|
+
optional: false
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.version_option
|
80
|
+
FastlaneCore::ConfigItem.new(
|
81
|
+
key: :version,
|
82
|
+
env_name: "MAESTRO_SCREENSHOTS_APP_VERSION",
|
83
|
+
description: "Version of the app that screenshots are taken from",
|
84
|
+
optional: false,
|
85
|
+
verify_block: proc do |value|
|
86
|
+
UI.user_error!("You must provide a version using the `version` parameter.") unless value && !value.strip.empty?
|
87
|
+
end
|
88
|
+
)
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.device_option
|
92
|
+
FastlaneCore::ConfigItem.new(
|
93
|
+
key: :device,
|
94
|
+
env_name: "MAESTRO_SCREENSHOTS_DEVICE",
|
95
|
+
description: "Device type: android or ios",
|
96
|
+
type: String,
|
97
|
+
optional: false,
|
98
|
+
verify_block: proc do |value|
|
99
|
+
UI.user_error!("You must specify a device type (android or ios).") unless %w[android ios].include?(value.downcase)
|
100
|
+
end
|
101
|
+
)
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.theme_option
|
105
|
+
FastlaneCore::ConfigItem.new(
|
106
|
+
key: :theme,
|
107
|
+
env_name: "MAESTRO_SCREENSHOTS_APPLICATION_THEME",
|
108
|
+
description: "Optional theme parameter (e.g., dark or light)",
|
109
|
+
default_value: nil,
|
110
|
+
optional: true
|
111
|
+
)
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.hmac_secret_option
|
115
|
+
FastlaneCore::ConfigItem.new(
|
116
|
+
key: :hmac_secret,
|
117
|
+
env_name: "MAESTRO_SCREENSHOTS_HMAC_SECRET",
|
118
|
+
description: "The HMAC secret used to sign the payload",
|
119
|
+
optional: false,
|
120
|
+
verify_block: proc do |value|
|
121
|
+
UI.user_error!("You must provide a valid HMAC secret using the `hmac_secret` parameter.") unless value && !value.strip.empty?
|
122
|
+
end
|
123
|
+
)
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.url_option
|
127
|
+
FastlaneCore::ConfigItem.new(
|
128
|
+
key: :url,
|
129
|
+
env_name: "MAESTRO_SCREENSHOTS_WEBHOOK_URL",
|
130
|
+
description: "The URL to send the API request to",
|
131
|
+
optional: false,
|
132
|
+
verify_block: proc do |value|
|
133
|
+
UI.user_error!("You must provide a valid URL using the `url` parameter.") unless value && !value.strip.empty?
|
134
|
+
UI.user_error!("The provided URL is invalid: #{value}") unless value.match?(URI::DEFAULT_PARSER.make_regexp)
|
135
|
+
end
|
136
|
+
)
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.is_supported?(platform)
|
140
|
+
true
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'fastlane/action'
|
2
|
+
require 'fastlane_core/configuration/config_item'
|
3
|
+
require_relative '../helper/maestro_orchestration_helper'
|
4
|
+
|
5
|
+
module Fastlane
|
6
|
+
module Actions
|
7
|
+
class MaestroOrchestrationIosAction < Action
|
8
|
+
def self.run(params)
|
9
|
+
required_params = [:scheme, :workspace, :maestro_flow_file]
|
10
|
+
missing_params = required_params.select { |param| params[param].nil? }
|
11
|
+
|
12
|
+
if missing_params.any?
|
13
|
+
missing_params.each do |param|
|
14
|
+
UI.error("Missing parameter: #{param}")
|
15
|
+
end
|
16
|
+
raise "Missing required parameters: #{missing_params.join(', ')}"
|
17
|
+
end
|
18
|
+
|
19
|
+
boot_ios_simulator(params)
|
20
|
+
demo_mode(params)
|
21
|
+
build_and_install_ios_app(params)
|
22
|
+
|
23
|
+
UI.message("Running Maestro tests on iOS...")
|
24
|
+
|
25
|
+
simulators_list = `xcrun simctl list devices`.strip
|
26
|
+
device_status = simulators_list.match(/#{Regexp.quote(params[:simulator_name])}.*\(([^)]+)\) \(([^)]+)\)/)
|
27
|
+
device_id = device_status[1]
|
28
|
+
`maestro --device #{device_id} test #{params[:maestro_flow_file]}`
|
29
|
+
UI.success("Finished Maestro tests on iOS.")
|
30
|
+
|
31
|
+
UI.message("Killing iOS simulator...")
|
32
|
+
system("xcrun simctl shutdown booted")
|
33
|
+
UI.success("iOS simulator killed. Process finished.")
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.boot_ios_simulator(params)
|
37
|
+
device_name = params[:simulator_name]
|
38
|
+
device_type = params[:device_type]
|
39
|
+
|
40
|
+
UI.message("Shutting down any booted iOS simulator...")
|
41
|
+
system("xcrun simctl shutdown booted")
|
42
|
+
|
43
|
+
UI.message("Checking if simulator '#{device_name}' exists...")
|
44
|
+
simulators_list = `xcrun simctl list devices -j`
|
45
|
+
simulator_data = JSON.parse(simulators_list)["devices"].values.flatten
|
46
|
+
|
47
|
+
existing_simulator = simulator_data.find { |sim| sim["name"] == device_name }
|
48
|
+
|
49
|
+
if existing_simulator
|
50
|
+
device_id = existing_simulator["udid"]
|
51
|
+
UI.message("Found existing simulator '#{device_name}' with ID #{device_id}. Deleting it...")
|
52
|
+
system("xcrun simctl delete #{device_id}")
|
53
|
+
end
|
54
|
+
|
55
|
+
UI.message("Creating a new simulator '#{device_name}'...")
|
56
|
+
system("xcrun simctl create '#{device_name}' #{device_type}")
|
57
|
+
|
58
|
+
# Refresh simulator list after creation
|
59
|
+
simulators_list = `xcrun simctl list devices -j`
|
60
|
+
new_simulator = JSON.parse(simulators_list)["devices"].values.flatten.find { |sim| sim["name"] == device_name }
|
61
|
+
|
62
|
+
unless new_simulator
|
63
|
+
UI.user_error!("Failed to create simulator '#{device_name}'.")
|
64
|
+
end
|
65
|
+
|
66
|
+
new_device_id = new_simulator["udid"]
|
67
|
+
UI.message("Booting the new simulator '#{device_name}' (ID: #{new_device_id})...")
|
68
|
+
system("xcrun simctl boot '#{new_device_id}'")
|
69
|
+
|
70
|
+
UI.message("Waiting for the simulator to fully boot...")
|
71
|
+
until `xcrun simctl list devices`.include?("#{device_name} (#{new_device_id}) (Booted)")
|
72
|
+
UI.message("Waiting for the simulator to boot...")
|
73
|
+
sleep(10)
|
74
|
+
end
|
75
|
+
|
76
|
+
UI.success("Simulator '#{device_name}' is booted and ready.")
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.demo_mode(params)
|
80
|
+
UI.message("Setting demo mode on #{params[:simulator_name]}...")
|
81
|
+
sh("xcrun simctl status_bar '#{params[:simulator_name]}' override --time '09:30'")
|
82
|
+
sh("xcrun simctl status_bar '#{params[:simulator_name]}' override --batteryState charged --batteryLevel 100")
|
83
|
+
sh("xcrun simctl status_bar '#{params[:simulator_name]}' override --wifiBars 3 --cellularBars 4")
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.build_and_install_ios_app(params)
|
87
|
+
UI.message("Building iOS app with scheme: #{params[:scheme]}")
|
88
|
+
other_action.gym(
|
89
|
+
workspace: params[:workspace],
|
90
|
+
scheme: params[:scheme],
|
91
|
+
destination: "platform=iOS Simulator,name=#{params[:simulator_name]}",
|
92
|
+
configuration: "Release",
|
93
|
+
clean: true,
|
94
|
+
sdk: "iphonesimulator",
|
95
|
+
build_path: "./build",
|
96
|
+
skip_archive: true,
|
97
|
+
skip_package_ipa: true,
|
98
|
+
include_symbols: false,
|
99
|
+
include_bitcode: false,
|
100
|
+
xcargs: "-UseModernBuildSystem=YES"
|
101
|
+
)
|
102
|
+
|
103
|
+
derived_data_path = File.expand_path("~/Library/Developer/Xcode/DerivedData")
|
104
|
+
app_path = Dir["#{derived_data_path}/**/Release-iphonesimulator/#{params[:scheme]}.app"].first
|
105
|
+
|
106
|
+
if app_path.nil?
|
107
|
+
UI.user_error!("Error: .app file not found in DerivedData.")
|
108
|
+
end
|
109
|
+
|
110
|
+
UI.message("Found .app file at: #{app_path}")
|
111
|
+
sh("xcrun simctl install booted '#{app_path}'")
|
112
|
+
UI.success("App installed on iOS simulator.")
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.description
|
116
|
+
"Boots an iOS simulator, builds the app, installs it, and runs Maestro tests"
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.available_options
|
120
|
+
[
|
121
|
+
FastlaneCore::ConfigItem.new(
|
122
|
+
key: :simulator_name,
|
123
|
+
env_name: "MAESTRO_IOS_DEVICE_NAME",
|
124
|
+
description: "The iOS simulator device to boot",
|
125
|
+
default_value: "iPhone 15",
|
126
|
+
optional: true,
|
127
|
+
type: String
|
128
|
+
),
|
129
|
+
FastlaneCore::ConfigItem.new(
|
130
|
+
key: :device_type,
|
131
|
+
env_name: "MAESTRO_IOS_DEVICE",
|
132
|
+
description: "The iOS simulator device type for new simulator",
|
133
|
+
default_value: "com.apple.CoreSimulator.SimDeviceType.iPhone-15",
|
134
|
+
optional: true,
|
135
|
+
type: String
|
136
|
+
),
|
137
|
+
FastlaneCore::ConfigItem.new(
|
138
|
+
key: :scheme,
|
139
|
+
env_name: "MAESTRO_IOS_SCHEME",
|
140
|
+
description: "The iOS app scheme to build",
|
141
|
+
optional: false,
|
142
|
+
type: String
|
143
|
+
),
|
144
|
+
FastlaneCore::ConfigItem.new(
|
145
|
+
key: :workspace,
|
146
|
+
env_name: "MAESTRO_IOS_WORKSPACE",
|
147
|
+
description: "The Xcode workspace",
|
148
|
+
optional: false,
|
149
|
+
type: String
|
150
|
+
),
|
151
|
+
FastlaneCore::ConfigItem.new(
|
152
|
+
key: :maestro_flow_file,
|
153
|
+
env_name: "MAESTRO_IOS_FLOW_FILE",
|
154
|
+
description: "The path to the Maestro flows YAML file",
|
155
|
+
optional: false,
|
156
|
+
type: String
|
157
|
+
)
|
158
|
+
]
|
159
|
+
end
|
160
|
+
|
161
|
+
def self.is_supported?(platform)
|
162
|
+
platform == :ios
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
data/lib/fastlane/plugin/maestro_orchestration/actions/maestro_orchestration_s3_upload_action.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'fastlane/action'
|
2
|
+
require 'aws-sdk-s3'
|
3
|
+
require 'fastlane_core/configuration/config_item'
|
4
|
+
require_relative '../helper/maestro_orchestration_helper'
|
5
|
+
|
6
|
+
module Fastlane
|
7
|
+
module Actions
|
8
|
+
class MaestroOrchestrationS3UploadAction < Action
|
9
|
+
def self.run(params)
|
10
|
+
required_params = [:folder_path, :bucket, :version, :device]
|
11
|
+
missing_params = required_params.select { |param| params[param].nil? }
|
12
|
+
|
13
|
+
if missing_params.any?
|
14
|
+
missing_params.each do |param|
|
15
|
+
UI.error("Missing parameter: #{param}")
|
16
|
+
end
|
17
|
+
raise "Missing required parameters: #{missing_params.join(', ')}"
|
18
|
+
end
|
19
|
+
|
20
|
+
UI.message("Uploading screenshots to S3...")
|
21
|
+
|
22
|
+
s3_client = Aws::S3::Client.new(
|
23
|
+
region: ENV.fetch('AWS_REGION', 'us-east-1')
|
24
|
+
)
|
25
|
+
|
26
|
+
base_path = "#{params[:s3_path]}/ver:#{params[:version]}"
|
27
|
+
base_path += "/theme:#{params[:theme]}" if params[:theme]
|
28
|
+
base_path += "/device:#{params[:device]}"
|
29
|
+
|
30
|
+
UI.message("Folder path: #{params[:folder_path]}")
|
31
|
+
Dir.glob("#{params[:folder_path]}/*").each do |file|
|
32
|
+
next if File.directory?(file)
|
33
|
+
|
34
|
+
file_name = File.basename(file)
|
35
|
+
s3_key = File.join(base_path, file_name)
|
36
|
+
|
37
|
+
UI.message("Uploading #{file} to s3://#{params[:bucket]}/#{s3_key}")
|
38
|
+
s3_client.put_object(bucket: params[:bucket], key: s3_key, body: File.open(file))
|
39
|
+
end
|
40
|
+
|
41
|
+
UI.success("Upload to S3 completed.")
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.description
|
45
|
+
"Uploads files to an S3 bucket."
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.available_options
|
49
|
+
[
|
50
|
+
FastlaneCore::ConfigItem.new(
|
51
|
+
key: :folder_path,
|
52
|
+
env_name: "MAESTRO_SCREENSHOTS_FOLDER_PATH",
|
53
|
+
description: "Path to the folder to be uploaded to S3",
|
54
|
+
optional: false,
|
55
|
+
verify_block: proc do |value|
|
56
|
+
UI.user_error!("You must provide a valid folder path using the `folder_path` parameter.") unless value && !value.strip.empty?
|
57
|
+
UI.user_error!("The folder path does not exist: #{value}") unless File.directory?(value)
|
58
|
+
end
|
59
|
+
),
|
60
|
+
FastlaneCore::ConfigItem.new(
|
61
|
+
key: :bucket,
|
62
|
+
env_name: "MAESTRO_SCREENSHOTS_S3_BUCKET",
|
63
|
+
description: "The S3 bucket name where files will be uploaded",
|
64
|
+
optional: false,
|
65
|
+
verify_block: proc do |value|
|
66
|
+
UI.user_error!("You must provide a valid S3 bucket name using the `bucket` parameter.") unless value && !value.strip.empty?
|
67
|
+
end
|
68
|
+
),
|
69
|
+
FastlaneCore::ConfigItem.new(
|
70
|
+
key: :s3_path,
|
71
|
+
env_name: "MAESTRO_SCREENSHOTS_S3_PATH",
|
72
|
+
description: "The base S3 path (after the bucket name) where files will be uploaded: $bucket/$s3_path",
|
73
|
+
optional: false
|
74
|
+
),
|
75
|
+
FastlaneCore::ConfigItem.new(
|
76
|
+
key: :version,
|
77
|
+
env_name: "MAESTRO_SCREENSHOTS_APP_VERSION",
|
78
|
+
description: "Version of the app that screenshots are taken from",
|
79
|
+
optional: false,
|
80
|
+
verify_block: proc do |value|
|
81
|
+
UI.user_error!("You must provide a version using the `version` parameter.") unless value && !value.strip.empty?
|
82
|
+
end
|
83
|
+
),
|
84
|
+
FastlaneCore::ConfigItem.new(
|
85
|
+
key: :device,
|
86
|
+
env_name: "MAESTRO_SCREENSHOTS_DEVICE",
|
87
|
+
description: "Device type: android or ios",
|
88
|
+
type: String,
|
89
|
+
optional: false
|
90
|
+
),
|
91
|
+
FastlaneCore::ConfigItem.new(
|
92
|
+
key: :theme,
|
93
|
+
env_name: "MAESTRO_SCREENSHOTS_APPLICATION_THEME",
|
94
|
+
description: "Optional theme parameter (e.g., dark or light)",
|
95
|
+
default_value: nil,
|
96
|
+
optional: true
|
97
|
+
)
|
98
|
+
]
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.is_supported?(platform)
|
102
|
+
true
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'fastlane_core/ui/ui'
|
2
|
+
require 'fastlane/action'
|
3
|
+
|
4
|
+
module Fastlane
|
5
|
+
UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
|
6
|
+
Helper = FastlaneCore::Helper unless Fastlane.const_defined?(:Helper)
|
7
|
+
|
8
|
+
module Helper
|
9
|
+
class MaestroOrchestrationHelper
|
10
|
+
# class methods that you define here become available in your action
|
11
|
+
# as `Helper::MaestroOrchestrationHelper.your_method`
|
12
|
+
#
|
13
|
+
def self.show_message
|
14
|
+
UI.message("Hello from the maestro_orchestration plugin helper!")
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.wait_for_emulator_to_boot(adb, max_retries, serial)
|
18
|
+
retries = 0
|
19
|
+
booted = false
|
20
|
+
|
21
|
+
while retries < max_retries
|
22
|
+
result = `#{adb.adb_path} -e shell getprop sys.boot_completed`.strip
|
23
|
+
UI.message("ADB Response (sys.boot_completed): #{result.inspect}")
|
24
|
+
|
25
|
+
if result == "1"
|
26
|
+
booted = true
|
27
|
+
break
|
28
|
+
elsif result.empty? || result.include?("device offline") || result.include?("device unauthorized")
|
29
|
+
UI.error("ADB issue detected: #{result}")
|
30
|
+
end
|
31
|
+
|
32
|
+
retries += 1
|
33
|
+
UI.message("Retrying... Attempt #{retries}/#{max_retries}")
|
34
|
+
|
35
|
+
wait_interval = [1 + (2**retries), 30].min
|
36
|
+
|
37
|
+
UI.message("Waiting for #{wait_interval} seconds before retrying...")
|
38
|
+
sleep(wait_interval)
|
39
|
+
end
|
40
|
+
|
41
|
+
booted
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class AvdHelper
|
46
|
+
# Path to the avd binary
|
47
|
+
attr_accessor :avdmanager_path
|
48
|
+
# Available AVDs
|
49
|
+
attr_accessor :avds
|
50
|
+
|
51
|
+
def initialize(avdmanager_path: nil)
|
52
|
+
android_home = ENV.fetch('ANDROID_HOME', nil) || ENV.fetch('ANDROID_SDK_ROOT', nil)
|
53
|
+
if (avdmanager_path.nil? || avdmanager_path == "avdmanager") && android_home
|
54
|
+
# First search for cmdline-tools dir
|
55
|
+
cmdline_tools_path = File.join(android_home, "cmdline-tools")
|
56
|
+
|
57
|
+
# Find the first available 'bin' folder within cmdline-tools
|
58
|
+
available_path = Dir.glob(File.join(cmdline_tools_path, "*", "bin")).first
|
59
|
+
raise "No valid bin path found in #{cmdline_tools_path}" unless available_path
|
60
|
+
|
61
|
+
avdmanager_path = File.join(available_path, "avdmanager")
|
62
|
+
end
|
63
|
+
|
64
|
+
self.avdmanager_path = Helper.get_executable_path(File.expand_path(avdmanager_path))
|
65
|
+
end
|
66
|
+
|
67
|
+
def trigger(command: nil)
|
68
|
+
raise "avdmanager_path is not set" unless avdmanager_path
|
69
|
+
|
70
|
+
# Build and execute the command
|
71
|
+
command = [avdmanager_path.shellescape, command].compact.join(" ").strip
|
72
|
+
Action.sh(command)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Create a new AVD
|
76
|
+
def create_avd(name:, package:, device: "pixel_7_pro")
|
77
|
+
raise "AVD name is required" if name.nil? || name.empty?
|
78
|
+
raise "System image package is required" if package.nil? || package.empty?
|
79
|
+
|
80
|
+
command = [
|
81
|
+
"create avd",
|
82
|
+
"-n #{name.shellescape}",
|
83
|
+
"-f",
|
84
|
+
"-k \"#{package}\"",
|
85
|
+
"-d #{device.shellescape}"
|
86
|
+
].join(" ")
|
87
|
+
|
88
|
+
trigger(command: command)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class EmulatorHelper
|
93
|
+
attr_accessor :emulator_path
|
94
|
+
|
95
|
+
def initialize(emulator_path: nil)
|
96
|
+
android_home = ENV.fetch('ANDROID_HOME', nil) || ENV.fetch('ANDROID_SDK_ROOT', nil)
|
97
|
+
if (emulator_path.nil? || emulator_path == "avdmanager") && android_home
|
98
|
+
emulator_path = File.join(android_home, "emulator", "emulator")
|
99
|
+
end
|
100
|
+
|
101
|
+
self.emulator_path = Helper.get_executable_path(File.expand_path(emulator_path))
|
102
|
+
end
|
103
|
+
|
104
|
+
def trigger(command: nil)
|
105
|
+
raise "emulator_path is not set" unless emulator_path
|
106
|
+
|
107
|
+
# Build and execute the command
|
108
|
+
command = [emulator_path.shellescape, command].compact.join(" ").strip
|
109
|
+
Action.sh(command)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Start an emulator instance
|
113
|
+
def start_emulator(name:, port:)
|
114
|
+
raise "Emulator name is required" if name.nil? || name.empty?
|
115
|
+
raise "Port is required" if port.nil? || port.to_s.empty?
|
116
|
+
|
117
|
+
command = [
|
118
|
+
"-avd #{name.shellescape}",
|
119
|
+
"-port #{port.shellescape}",
|
120
|
+
"-wipe-data",
|
121
|
+
"-no-boot-anim",
|
122
|
+
"-no-snapshot",
|
123
|
+
"-no-audio",
|
124
|
+
"> /dev/null 2>&1 &"
|
125
|
+
].join(" ")
|
126
|
+
|
127
|
+
UI.message("Starting emulator #{name} on port #{port}...")
|
128
|
+
trigger(command: command)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'fastlane/plugin/maestro_orchestration/version'
|
2
|
+
|
3
|
+
module Fastlane
|
4
|
+
module MaestroOrchestration
|
5
|
+
# Return all .rb files inside the "actions" and "helper" directory
|
6
|
+
def self.all_classes
|
7
|
+
Dir[File.expand_path('**/{actions,helper}/*.rb', File.dirname(__FILE__))]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# By default we want to import all available actions and helpers
|
13
|
+
# A plugin can contain any number of actions and plugins
|
14
|
+
Fastlane::MaestroOrchestration.all_classes.each do |current|
|
15
|
+
require current
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fastlane-plugin-maestro_orchestration
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Nemanja Risteski
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-01-31 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: fastlane-plugin-android_emulator
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.2'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.2.1
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.2'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.2.1
|
33
|
+
description:
|
34
|
+
email: nemanja.risteski@sourcetoad.com
|
35
|
+
executables: []
|
36
|
+
extensions: []
|
37
|
+
extra_rdoc_files: []
|
38
|
+
files:
|
39
|
+
- LICENSE
|
40
|
+
- README.md
|
41
|
+
- lib/fastlane/plugin/maestro_orchestration.rb
|
42
|
+
- lib/fastlane/plugin/maestro_orchestration/actions/maestro_orchestration_android_action.rb
|
43
|
+
- lib/fastlane/plugin/maestro_orchestration/actions/maestro_orchestration_api_request_action.rb
|
44
|
+
- lib/fastlane/plugin/maestro_orchestration/actions/maestro_orchestration_ios_action.rb
|
45
|
+
- lib/fastlane/plugin/maestro_orchestration/actions/maestro_orchestration_s3_upload_action.rb
|
46
|
+
- lib/fastlane/plugin/maestro_orchestration/helper/maestro_orchestration_helper.rb
|
47
|
+
- lib/fastlane/plugin/maestro_orchestration/version.rb
|
48
|
+
homepage: https://github.com/sourcetoad/fastlane-plugin-maestro_orchestration
|
49
|
+
licenses:
|
50
|
+
- MIT
|
51
|
+
metadata:
|
52
|
+
rubygems_mfa_required: 'true'
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options: []
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.6'
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '0'
|
67
|
+
requirements: []
|
68
|
+
rubygems_version: 3.1.6
|
69
|
+
signing_key:
|
70
|
+
specification_version: 4
|
71
|
+
summary: Plugin for maestro testing framework.
|
72
|
+
test_files: []
|