fastlane-plugin-mad 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e77f27ccff773cb7dcbd32ecc90cd8fe29ea0c31a9d22126e1ecdcc4132ea64c
4
+ data.tar.gz: 8f1c2bd9747a9cfe80b54d9fad59be8f10471e3ee0102b7e6c533db3edf677bd
5
+ SHA512:
6
+ metadata.gz: 3045c80d042167fd4e6775c4f1e02a7f30e8c812401c6ae3efd5d40a200a92bbec851f7fbf711c26413f2750f5281c6c58f3f4182e74816d987427d5dcc40f2a
7
+ data.tar.gz: 8c70d232302c478a2ac9f196af0a499f54c286407b1c04bfe4cc885ebf6d3de901b118f127e4d9a87469cf312d4ea90b3d67b6b37eeed93ec1ed97d2408e8377
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Simon Sarrafi
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,44 @@
1
+ # fastlane-plugin-mad
2
+
3
+ Upload builds to MAD (TestFairy-compatible upload endpoint).
4
+
5
+ ## Installation
6
+
7
+ Add the plugin to your project's `Pluginfile`:
8
+
9
+ ```ruby
10
+ gem 'fastlane-plugin-mad', path: '/path/to/fastlane-plugin-mad'
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ruby
16
+ mad(
17
+ api_key: "your_api_key",
18
+ ipa: "./path/to/app.ipa",
19
+ comment: "Build #{lane_context[SharedValues::BUILD_NUMBER]}"
20
+ )
21
+ ```
22
+
23
+ ## Parameters
24
+
25
+ | Key | Description | Default |
26
+ |-----|-------------|---------|
27
+ | `api_key` | API Key for MAD | — |
28
+ | `ipa` | Path to your IPA file (iOS) | — |
29
+ | `apk` | Path to your APK file (Android) | — |
30
+ | `symbols_file` | Symbols mapping file | — |
31
+ | `upload_url` | API URL for MAD | `https://app.testfairy.com` |
32
+ | `testers_groups` | Array of tester groups | `[]` |
33
+ | `metrics` | Array of metrics to record | `[]` |
34
+ | `comment` | Additional release notes | `No comment provided` |
35
+ | `auto_update` | Auto-upgrade users (`on`/`off`) | `off` |
36
+ | `notify` | Send email to testers (`on`/`off`) | `off` |
37
+ | `options` | Array of options | `[]` |
38
+ | `custom` | Custom options string | `""` |
39
+ | `timeout` | Request timeout in seconds | — |
40
+ | `tags` | Custom tags for builds | `[]` |
41
+ | `folder_name` | Dashboard folder name | `""` |
42
+ | `landing_page_mode` | Landing page visibility (`open`/`closed`) | `open` |
43
+ | `upload_to_saucelabs` | Upload to Sauce Labs (`on`/`off`) | `off` |
44
+ | `platform` | Platform override | `""` |
@@ -0,0 +1,317 @@
1
+ module Fastlane
2
+ module Actions
3
+ module SharedValues
4
+ MAD_BUILD_URL = :MAD_BUILD_URL
5
+ MAD_DOWNLOAD_URL = :MAD_DOWNLOAD_URL
6
+ MAD_LANDING_PAGE = :MAD_LANDING_PAGE
7
+ end
8
+
9
+ class MadAction < Action
10
+ def self.upload_build(upload_url, ipa, options, timeout)
11
+ require 'faraday'
12
+ require 'faraday_middleware'
13
+
14
+ UI.success("Uploading to #{upload_url}...")
15
+
16
+ connection = Faraday.new(url: upload_url) do |builder|
17
+ builder.request(:multipart)
18
+ builder.request(:url_encoded)
19
+ builder.request(:retry, max: 3, interval: 5)
20
+ builder.response(:json, content_type: /\bjson$/)
21
+ builder.use(FaradayMiddleware::FollowRedirects)
22
+ builder.adapter(:net_http)
23
+ end
24
+
25
+ options[:file] = Faraday::UploadIO.new(ipa, 'application/octet-stream') if ipa && File.exist?(ipa)
26
+
27
+ symbols_file = options.delete(:symbols_file)
28
+ if symbols_file
29
+ options[:symbols_file] = Faraday::UploadIO.new(symbols_file, 'application/octet-stream')
30
+ end
31
+
32
+ begin
33
+ connection.post do |req|
34
+ req.options.timeout = timeout
35
+ req.url("/api/upload/")
36
+ req.body = options
37
+ end
38
+ rescue Faraday::TimeoutError
39
+ UI.crash!("Uploading build to MAD timed out ⏳")
40
+ end
41
+ end
42
+
43
+ def self.run(params)
44
+ UI.success('Starting with ipa upload to MAD...')
45
+
46
+ metrics_to_client = lambda do |metrics|
47
+ metrics.map do |metric|
48
+ case metric
49
+ when :cpu, :memory, :network, :gps, :battery, :mic, :wifi
50
+ metric.to_s
51
+ when :phone_signal
52
+ 'phone-signal'
53
+ else
54
+ UI.user_error!("Unknown metric: #{metric}")
55
+ end
56
+ end
57
+ end
58
+
59
+ options_to_client = lambda do |options|
60
+ options.map do |option|
61
+ case option.to_sym
62
+ when :shake, :anonymous
63
+ option.to_s
64
+ when :video_only_wifi
65
+ 'video-only-wifi'
66
+ else
67
+ UI.user_error!("Unknown option: #{option}")
68
+ end
69
+ end
70
+ end
71
+
72
+ # Rejecting key `upload_url` and `timeout` as we don't need it in options
73
+ client_options = Hash[params.values.reject do |key, value|
74
+ [:upload_url, :timeout].include?(key)
75
+ end.map do |key, value|
76
+ case key
77
+ when :api_key
78
+ [key, value]
79
+ when :ipa
80
+ [key, value]
81
+ when :apk
82
+ [key, value]
83
+ when :symbols_file
84
+ [key, value]
85
+ when :testers_groups
86
+ [key, value.join(',')]
87
+ when :metrics
88
+ [key, metrics_to_client.call(value).join(',')]
89
+ when :comment
90
+ [key, value]
91
+ when :auto_update
92
+ ['auto-update', value]
93
+ when :notify
94
+ [key, value]
95
+ when :options
96
+ [key, options_to_client.call(value).join(',')]
97
+ when :custom
98
+ [key, value]
99
+ when :tags
100
+ [key, value.join(',')]
101
+ when :folder_name
102
+ [key, value]
103
+ when :landing_page_mode
104
+ [key, value]
105
+ when :upload_to_saucelabs
106
+ [key, value]
107
+ when :platform
108
+ [key, value]
109
+ else
110
+ UI.user_error!("Unknown parameter: #{key}")
111
+ end
112
+ end]
113
+
114
+ path = params[:ipa] || params[:apk]
115
+ UI.user_error!("No ipa or apk were given") unless path
116
+
117
+ return path if Helper.test?
118
+
119
+ response = self.upload_build(params[:upload_url], path, client_options, params[:timeout])
120
+ if parse_response(response)
121
+ UI.success("Build URL: #{Actions.lane_context[SharedValues::MAD_BUILD_URL]}")
122
+ UI.success("Download URL: #{Actions.lane_context[SharedValues::MAD_DOWNLOAD_URL]}")
123
+ UI.success("Landing Page URL: #{Actions.lane_context[SharedValues::MAD_LANDING_PAGE]}")
124
+ UI.success("Build successfully uploaded to MAD.")
125
+ else
126
+ UI.user_error!("Error when trying to upload ipa to MAD")
127
+ end
128
+ end
129
+
130
+ def self.parse_response(response)
131
+ if response.body && response.body.key?('status') && response.body['status'] == 'ok'
132
+ build_url = response.body['build_url']
133
+ app_url = response.body['app_url']
134
+ landing_page_url = response.body['landing_page_url']
135
+
136
+ Actions.lane_context[SharedValues::MAD_BUILD_URL] = build_url
137
+ Actions.lane_context[SharedValues::MAD_DOWNLOAD_URL] = app_url
138
+ Actions.lane_context[SharedValues::MAD_LANDING_PAGE] = landing_page_url
139
+
140
+ return true
141
+ else
142
+ UI.error("Error uploading to MAD: #{response.body}")
143
+
144
+ return false
145
+ end
146
+ end
147
+ private_class_method :parse_response
148
+
149
+ def self.description
150
+ 'Upload a new build to MAD'
151
+ end
152
+
153
+ def self.details
154
+ 'Upload a new build to MAD (TestFairy-compatible upload endpoint).'
155
+ end
156
+
157
+ def self.available_options
158
+ [
159
+ # required
160
+ FastlaneCore::ConfigItem.new(key: :api_key,
161
+ env_name: "FL_MAD_API_KEY",
162
+ description: "API Key for MAD",
163
+ sensitive: true,
164
+ verify_block: proc do |value|
165
+ UI.user_error!("No API key for MAD given, pass using `api_key: 'key'`") unless value.to_s.length > 0
166
+ end),
167
+ FastlaneCore::ConfigItem.new(key: :ipa,
168
+ env_name: 'MAD_IPA_PATH',
169
+ description: 'Path to your IPA file for iOS',
170
+ default_value: Actions.lane_context[SharedValues::IPA_OUTPUT_PATH],
171
+ default_value_dynamic: true,
172
+ optional: true,
173
+ conflicting_options: [:apk],
174
+ verify_block: proc do |value|
175
+ UI.user_error!("Couldn't find ipa file at path '#{value}'") unless File.exist?(value)
176
+ end),
177
+ FastlaneCore::ConfigItem.new(key: :apk,
178
+ env_name: 'MAD_APK_PATH',
179
+ description: 'Path to your APK file for Android',
180
+ default_value: Actions.lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH],
181
+ default_value_dynamic: true,
182
+ optional: true,
183
+ conflicting_options: [:ipa],
184
+ verify_block: proc do |value|
185
+ UI.user_error!("Couldn't find apk file at path '#{value}'") unless File.exist?(value)
186
+ end),
187
+ # optional
188
+ FastlaneCore::ConfigItem.new(key: :symbols_file,
189
+ optional: true,
190
+ env_name: "FL_MAD_SYMBOLS_FILE",
191
+ description: "Symbols mapping file",
192
+ default_value: Actions.lane_context[SharedValues::DSYM_OUTPUT_PATH],
193
+ default_value_dynamic: true,
194
+ verify_block: proc do |value|
195
+ UI.user_error!("Couldn't find dSYM file at path '#{value}'") unless File.exist?(value)
196
+ end),
197
+ FastlaneCore::ConfigItem.new(key: :upload_url,
198
+ env_name: "FL_MAD_UPLOAD_URL",
199
+ description: "API URL for MAD",
200
+ default_value: "https://app.testfairy.com",
201
+ optional: true),
202
+ FastlaneCore::ConfigItem.new(key: :testers_groups,
203
+ optional: true,
204
+ type: Array,
205
+ short_option: '-g',
206
+ env_name: "FL_MAD_TESTERS_GROUPS",
207
+ description: "Array of tester groups to be notified",
208
+ default_value: []),
209
+ FastlaneCore::ConfigItem.new(key: :metrics,
210
+ optional: true,
211
+ type: Array,
212
+ env_name: "FL_MAD_METRICS",
213
+ description: "Array of metrics to record (cpu,memory,network,phone_signal,gps,battery,mic,wifi)",
214
+ default_value: []),
215
+ FastlaneCore::ConfigItem.new(key: :comment,
216
+ optional: true,
217
+ env_name: "FL_MAD_COMMENT",
218
+ description: "Additional release notes for this upload. This text will be added to email notifications",
219
+ default_value: 'No comment provided'),
220
+ FastlaneCore::ConfigItem.new(key: :auto_update,
221
+ optional: true,
222
+ env_name: "FL_MAD_AUTO_UPDATE",
223
+ description: "Allows an easy upgrade of all users to the current version. To enable set to 'on'",
224
+ default_value: 'off'),
225
+ FastlaneCore::ConfigItem.new(key: :notify,
226
+ optional: true,
227
+ env_name: "FL_MAD_NOTIFY",
228
+ description: "Send email to testers",
229
+ default_value: 'off'),
230
+ FastlaneCore::ConfigItem.new(key: :options,
231
+ optional: true,
232
+ type: Array,
233
+ env_name: "FL_MAD_OPTIONS",
234
+ description: "Array of options (shake,video_only_wifi,anonymous)",
235
+ default_value: []),
236
+ FastlaneCore::ConfigItem.new(key: :custom,
237
+ optional: true,
238
+ env_name: "FL_MAD_CUSTOM",
239
+ description: "Array of custom options. Contact support for more information",
240
+ default_value: ''),
241
+ FastlaneCore::ConfigItem.new(key: :timeout,
242
+ env_name: "FL_MAD_TIMEOUT",
243
+ description: "Request timeout in seconds",
244
+ type: Integer,
245
+ optional: true),
246
+ FastlaneCore::ConfigItem.new(key: :tags,
247
+ optional: true,
248
+ env_name: "FL_MAD_TAGS",
249
+ description: "Custom tags that can be used to organize your builds",
250
+ type: Array,
251
+ default_value: []),
252
+ FastlaneCore::ConfigItem.new(key: :folder_name,
253
+ optional: true,
254
+ env_name: "FL_MAD_FOLDER_NAME",
255
+ description: "Name of the dashboard folder that contains this app",
256
+ default_value: ''),
257
+ FastlaneCore::ConfigItem.new(key: :landing_page_mode,
258
+ optional: true,
259
+ env_name: "FL_MAD_LANDING_PAGE_MODE",
260
+ description: "Visibility of build landing after upload. Can be 'open' or 'closed'",
261
+ default_value: 'open',
262
+ verify_block: proc do |value|
263
+ UI.user_error!("The landing page mode can only be open or closed") unless %w(open closed).include?(value)
264
+ end),
265
+ FastlaneCore::ConfigItem.new(key: :upload_to_saucelabs,
266
+ optional: true,
267
+ env_name: "FL_MAD_UPLOAD_TO_SAUCELABS",
268
+ description: "Upload file directly to Sauce Labs. It can be 'on' or 'off'",
269
+ default_value: 'off',
270
+ verify_block: proc do |value|
271
+ UI.user_error!("The upload to Sauce Labs can only be on or off") unless %w(on off).include?(value)
272
+ end),
273
+ FastlaneCore::ConfigItem.new(key: :platform,
274
+ optional: true,
275
+ env_name: "FL_MAD_PLATFORM",
276
+ description: "Use if upload build is not iOS or Android. Contact support for more information",
277
+ default_value: '')
278
+ ]
279
+ end
280
+
281
+ def self.example_code
282
+ [
283
+ 'mad(
284
+ api_key: "...",
285
+ ipa: "./ipa_file.ipa",
286
+ comment: "Build #{lane_context[SharedValues::BUILD_NUMBER]}",
287
+ )',
288
+ 'mad(
289
+ api_key: "...",
290
+ apk: "../build/app/outputs/apk/qa/release/app-qa-release.apk",
291
+ comment: "Build #{lane_context[SharedValues::BUILD_NUMBER]}",
292
+ )'
293
+ ]
294
+ end
295
+
296
+ def self.category
297
+ :beta
298
+ end
299
+
300
+ def self.output
301
+ [
302
+ ['MAD_BUILD_URL', 'URL for the sessions of the newly uploaded build'],
303
+ ['MAD_DOWNLOAD_URL', 'URL directly to the newly uploaded build'],
304
+ ['MAD_LANDING_PAGE', "URL of the build's landing page"]
305
+ ]
306
+ end
307
+
308
+ def self.authors
309
+ ["mad"]
310
+ end
311
+
312
+ def self.is_supported?(platform)
313
+ [:ios, :android].include?(platform)
314
+ end
315
+ end
316
+ end
317
+ end
@@ -0,0 +1,13 @@
1
+ require 'fastlane_core/ui/ui'
2
+
3
+ module Fastlane
4
+ UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
5
+
6
+ module Helper
7
+ class MadHelper
8
+ def self.show_message
9
+ UI.message("Hello from the mad plugin helper!")
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require 'fastlane/plugin/mad/version'
2
+
3
+ module Fastlane
4
+ module Mad
5
+ def self.all_classes
6
+ Dir[File.expand_path('**/{actions,helper}/*.rb', File.dirname(__FILE__))]
7
+ end
8
+ end
9
+ end
10
+
11
+ Fastlane::Mad.all_classes.each do |current|
12
+ require current
13
+ end
@@ -0,0 +1,5 @@
1
+ module Fastlane
2
+ module Mad
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1 @@
1
+ require 'fastlane/plugin/mad/mad'
metadata ADDED
@@ -0,0 +1,161 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fastlane-plugin-mad
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sauce Labs
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-02-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday_middleware
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: fastlane
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '2.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '2.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '='
116
+ - !ruby/object:Gem::Version
117
+ version: 1.50.2
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '='
123
+ - !ruby/object:Gem::Version
124
+ version: 1.50.2
125
+ description:
126
+ email: mobileappdistribution@saucelabs.com
127
+ executables: []
128
+ extensions: []
129
+ extra_rdoc_files: []
130
+ files:
131
+ - LICENSE
132
+ - README.md
133
+ - lib/fastlane/plugin/mad.rb
134
+ - lib/fastlane/plugin/mad/actions/mad_action.rb
135
+ - lib/fastlane/plugin/mad/helper/mad_helper.rb
136
+ - lib/fastlane/plugin/mad/mad.rb
137
+ - lib/fastlane/plugin/mad/version.rb
138
+ homepage: https://saucelabs.com/products/mobile-testing/app-betas
139
+ licenses:
140
+ - MIT
141
+ metadata: {}
142
+ post_install_message:
143
+ rdoc_options: []
144
+ require_paths:
145
+ - lib
146
+ required_ruby_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: '2.6'
151
+ required_rubygems_version: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ requirements: []
157
+ rubygems_version: 3.4.1
158
+ signing_key:
159
+ specification_version: 4
160
+ summary: Upload builds to MAD (TestFairy-compatible)
161
+ test_files: []