fastlane-plugin-betadrop 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 +58 -0
- data/lib/fastlane/plugin/betadrop/actions/betadrop_action.rb +154 -0
- data/lib/fastlane/plugin/betadrop/actions/betadrop_inspect_action.rb +78 -0
- data/lib/fastlane/plugin/betadrop/helper/betadrop_helper.rb +143 -0
- data/lib/fastlane/plugin/betadrop/version.rb +5 -0
- data/lib/fastlane/plugin/betadrop.rb +14 -0
- metadata +53 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8ce7c339c0a0073c167fda130b72ccd3bab1565ec12866aac66d6343b7b429f7
|
|
4
|
+
data.tar.gz: 9d13fdd00f6549f200e5f93ca30a3de75316757a69bb20dce441e8d3ca7545cf
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5ec6c7a516786704d3c2dada1e775334949059f0550d5a0570e680c07dbafe285c69532426db556cec3bac521d3f4dc15096ee289cecd03f69153224acc52212
|
|
7
|
+
data.tar.gz: 69c1fac304243b36d47c3039ddaaf36756399c7d18a29d25c00c8184788f5794f737bd860dc65b3ec161255fb5cf31dc33744bdc40d14f65a933d05b097700cb
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BetaDrop
|
|
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,58 @@
|
|
|
1
|
+
# fastlane-plugin-betadrop
|
|
2
|
+
|
|
3
|
+
Upload the `.ipa` or `.apk` your lane just built to [BetaDrop](https://betadrop.app) and get an
|
|
4
|
+
over-the-air install link — no TestFlight review, no Play track, no tester accounts.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
fastlane add_plugin betadrop
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
lane :beta do
|
|
14
|
+
gym(export_method: "ad-hoc")
|
|
15
|
+
betadrop(
|
|
16
|
+
notes: last_git_commit[:message],
|
|
17
|
+
standing_link: "acmebeta", # optional: one URL testers keep (free: 1, Pro/Studio: up to 50)
|
|
18
|
+
inspect: true # refuse to upload a build that will not install from a link
|
|
19
|
+
)
|
|
20
|
+
slack(message: "New build: #{lane_context[SharedValues::BETADROP_INSTALL_URL]}")
|
|
21
|
+
end
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Set `BETADROP_TOKEN` in your CI secrets (create one at betadrop.app → Settings → Developer → API
|
|
25
|
+
tokens, or with `npx @betadrop/cli tokens create ci`). With no `file:`, the action uploads what
|
|
26
|
+
`gym` or `gradle` produced.
|
|
27
|
+
|
|
28
|
+
## Actions
|
|
29
|
+
|
|
30
|
+
### `betadrop`
|
|
31
|
+
|
|
32
|
+
| Option | Env | Description |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `api_token` | `BETADROP_TOKEN` | API token. Required. |
|
|
35
|
+
| `file` | `BETADROP_FILE` | Path to the build. Defaults to `IPA_OUTPUT_PATH` / `GRADLE_APK_OUTPUT_PATH`. |
|
|
36
|
+
| `name` | | Override the build name. |
|
|
37
|
+
| `notes` | `BETADROP_NOTES` | Release notes shown on the install page. |
|
|
38
|
+
| `standing_link` | `BETADROP_STANDING_LINK` | Standing link slug to point at the build. Fails the lane if it could not be moved. |
|
|
39
|
+
| `expires_in_days` · `max_downloads` · `max_devices` | | Limits on the install link; with more than one, it stops at whichever comes first. |
|
|
40
|
+
| `no_expiry` | | Keep the link until the build is deleted (paid plans). |
|
|
41
|
+
| `app_version` · `build_number` · `bundle_id` | | Override what is read from the build. |
|
|
42
|
+
| `inspect` | | Check the build first and fail, with nothing uploaded, if it will not install from a link. |
|
|
43
|
+
| `api_url` | `BETADROP_API_URL` | API host. Default `https://api.betadrop.app`. |
|
|
44
|
+
|
|
45
|
+
Sets `BETADROP_INSTALL_URL`, `BETADROP_BUILD_ID` and `BETADROP_STANDING_LINK_URL` in
|
|
46
|
+
`lane_context`, and returns the install URL.
|
|
47
|
+
|
|
48
|
+
### `betadrop_inspect`
|
|
49
|
+
|
|
50
|
+
Checks a build without uploading it to your account: provisioning profile type and expiry, a
|
|
51
|
+
profile issued for another bundle ID, unsigned or `testOnly` APKs, v1-only signatures on apps
|
|
52
|
+
targeting Android 11+. A read-only token is enough. `strict: true` fails on warnings too;
|
|
53
|
+
`fail_on_error: false` only reports. The report is in `BETADROP_INSPECT_REPORT`.
|
|
54
|
+
|
|
55
|
+
## Also
|
|
56
|
+
|
|
57
|
+
- Webhooks to Slack, Discord or your own service: <https://betadrop.app/docs/webhooks/>
|
|
58
|
+
- The CLI the GitHub Action uses: <https://betadrop.app/docs/cli/>
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
require 'fastlane/action'
|
|
2
|
+
require_relative '../helper/betadrop_helper'
|
|
3
|
+
|
|
4
|
+
module Fastlane
|
|
5
|
+
module Actions
|
|
6
|
+
module SharedValues
|
|
7
|
+
BETADROP_INSTALL_URL = :BETADROP_INSTALL_URL
|
|
8
|
+
BETADROP_BUILD_ID = :BETADROP_BUILD_ID
|
|
9
|
+
BETADROP_STANDING_LINK_URL = :BETADROP_STANDING_LINK_URL
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class BetadropAction < Action
|
|
13
|
+
def self.run(params)
|
|
14
|
+
file = params[:file] ||
|
|
15
|
+
Actions.lane_context[SharedValues::IPA_OUTPUT_PATH] ||
|
|
16
|
+
Actions.lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH]
|
|
17
|
+
UI.user_error!('No build to upload. Pass file:, or run gym / gradle first.') if file.to_s.empty?
|
|
18
|
+
|
|
19
|
+
client = Helper::BetadropHelper.new(api_token: params[:api_token], api_url: params[:api_url])
|
|
20
|
+
|
|
21
|
+
if params[:inspect]
|
|
22
|
+
UI.message("Checking #{File.basename(file)} will install from a link…")
|
|
23
|
+
report = client.inspect_build(file)
|
|
24
|
+
(report['checks'] || []).each do |check|
|
|
25
|
+
case check['level']
|
|
26
|
+
when 'error' then UI.error(check['message'])
|
|
27
|
+
when 'warning' then UI.important(check['message'])
|
|
28
|
+
else UI.success(check['message'])
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
unless report['installableFromLink']
|
|
32
|
+
UI.user_error!("#{File.basename(file)} will not install from a link, so it was not uploaded. Fix the errors above.")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
fields = {
|
|
37
|
+
'name' => params[:name],
|
|
38
|
+
'notes' => params[:notes],
|
|
39
|
+
'version' => params[:app_version],
|
|
40
|
+
'buildNumber' => params[:build_number],
|
|
41
|
+
'bundleId' => params[:bundle_id],
|
|
42
|
+
'channel' => params[:standing_link]
|
|
43
|
+
}.merge(Helper::BetadropHelper.expiry_fields(
|
|
44
|
+
days: params[:expires_in_days],
|
|
45
|
+
downloads: params[:max_downloads],
|
|
46
|
+
devices: params[:max_devices],
|
|
47
|
+
permanent: params[:no_expiry]
|
|
48
|
+
))
|
|
49
|
+
|
|
50
|
+
UI.message("Uploading #{File.basename(file)} to BetaDrop…")
|
|
51
|
+
result = client.publish(file, fields)
|
|
52
|
+
|
|
53
|
+
app_url = (ENV['BETADROP_APP_URL'] || 'https://betadrop.app').sub(%r{/+\z}, '')
|
|
54
|
+
install_url = "#{app_url}/install/?i=#{result['shortId']}"
|
|
55
|
+
Actions.lane_context[SharedValues::BETADROP_INSTALL_URL] = install_url
|
|
56
|
+
Actions.lane_context[SharedValues::BETADROP_BUILD_ID] = result['id']
|
|
57
|
+
|
|
58
|
+
UI.important(result['retentionClamp']['message']) if result['retentionClamp']
|
|
59
|
+
UI.important("#{result['duplicateOf']['message']} (#{result['duplicateOf']['url']})") if result['duplicateOf']
|
|
60
|
+
|
|
61
|
+
if result['channel']
|
|
62
|
+
Actions.lane_context[SharedValues::BETADROP_STANDING_LINK_URL] = result['channel']['url']
|
|
63
|
+
UI.success("Standing link #{result['channel']['slug']} now serves this build: #{result['channel']['url']}")
|
|
64
|
+
end
|
|
65
|
+
UI.success("Install link: #{install_url}")
|
|
66
|
+
# A standing link that was asked for and did not move is a failed release, even though the
|
|
67
|
+
# build itself is live — the same rule the CLI and the GitHub Action apply.
|
|
68
|
+
UI.user_error!(result['channelWarning']) if result['channelWarning']
|
|
69
|
+
|
|
70
|
+
install_url
|
|
71
|
+
rescue Helper::BetadropHelper::Error => e
|
|
72
|
+
UI.user_error!(e.message)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.description
|
|
76
|
+
'Upload an .ipa or .apk to BetaDrop and get an over-the-air install link'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.details
|
|
80
|
+
'Publishes the build gym or gradle just produced. Optionally checks it will install from a ' \
|
|
81
|
+
'link first, points a standing link at it, and limits how long or how widely the link works.'
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.authors
|
|
85
|
+
['BetaDrop']
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def self.return_value
|
|
89
|
+
'The install URL'
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.output
|
|
93
|
+
[
|
|
94
|
+
['BETADROP_INSTALL_URL', 'Install link for the uploaded build'],
|
|
95
|
+
['BETADROP_BUILD_ID', 'BetaDrop build id'],
|
|
96
|
+
['BETADROP_STANDING_LINK_URL', 'The standing link URL, when standing_link was set']
|
|
97
|
+
]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def self.available_options
|
|
101
|
+
positive = proc do |name|
|
|
102
|
+
proc do |value|
|
|
103
|
+
UI.user_error!("#{name} must be a whole number of 1 or more") unless value.to_s =~ /\A[1-9]\d*\z/
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
[
|
|
107
|
+
FastlaneCore::ConfigItem.new(key: :api_token, env_name: 'BETADROP_TOKEN', sensitive: true,
|
|
108
|
+
description: 'BetaDrop API token (bd_live_…)', optional: false),
|
|
109
|
+
FastlaneCore::ConfigItem.new(key: :file, env_name: 'BETADROP_FILE', optional: true,
|
|
110
|
+
description: 'Path to the .ipa or .apk. Defaults to the output of gym or gradle'),
|
|
111
|
+
FastlaneCore::ConfigItem.new(key: :name, optional: true, description: 'Override the build name'),
|
|
112
|
+
FastlaneCore::ConfigItem.new(key: :notes, env_name: 'BETADROP_NOTES', optional: true,
|
|
113
|
+
description: 'Release notes shown on the install page'),
|
|
114
|
+
FastlaneCore::ConfigItem.new(key: :standing_link, env_name: 'BETADROP_STANDING_LINK', optional: true,
|
|
115
|
+
description: 'Standing link slug to point at this build (must already be claimed)'),
|
|
116
|
+
FastlaneCore::ConfigItem.new(key: :expires_in_days, optional: true, type: Integer,
|
|
117
|
+
verify_block: positive.call('expires_in_days'),
|
|
118
|
+
description: 'Stop the install link working after this many days'),
|
|
119
|
+
FastlaneCore::ConfigItem.new(key: :max_downloads, optional: true, type: Integer,
|
|
120
|
+
verify_block: positive.call('max_downloads'),
|
|
121
|
+
description: 'Stop the install link after this many downloads'),
|
|
122
|
+
FastlaneCore::ConfigItem.new(key: :max_devices, optional: true, type: Integer,
|
|
123
|
+
verify_block: positive.call('max_devices'),
|
|
124
|
+
description: 'Stop the install link after this many devices'),
|
|
125
|
+
FastlaneCore::ConfigItem.new(key: :no_expiry, optional: true, type: Boolean, default_value: false,
|
|
126
|
+
description: 'Keep the link working until the build is deleted (paid plans)'),
|
|
127
|
+
FastlaneCore::ConfigItem.new(key: :app_version, optional: true, description: 'Override the version string'),
|
|
128
|
+
FastlaneCore::ConfigItem.new(key: :build_number, optional: true, description: 'Override the build number'),
|
|
129
|
+
FastlaneCore::ConfigItem.new(key: :bundle_id, optional: true, description: 'Override the bundle / package id'),
|
|
130
|
+
FastlaneCore::ConfigItem.new(key: :inspect, optional: true, type: Boolean, default_value: false,
|
|
131
|
+
description: 'Check the build will install from a link before uploading, and fail if not'),
|
|
132
|
+
FastlaneCore::ConfigItem.new(key: :api_url, env_name: 'BETADROP_API_URL', optional: true,
|
|
133
|
+
default_value: 'https://api.betadrop.app', description: 'API host')
|
|
134
|
+
]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def self.is_supported?(platform)
|
|
138
|
+
%i[ios android].include?(platform)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def self.example_code
|
|
142
|
+
[
|
|
143
|
+
'betadrop',
|
|
144
|
+
'betadrop(notes: last_git_commit[:message], standing_link: "acmebeta", inspect: true)',
|
|
145
|
+
'betadrop(file: "app/build/outputs/apk/release/app-release.apk", max_devices: 25)'
|
|
146
|
+
]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def self.category
|
|
150
|
+
:beta
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require 'fastlane/action'
|
|
2
|
+
require_relative '../helper/betadrop_helper'
|
|
3
|
+
|
|
4
|
+
module Fastlane
|
|
5
|
+
module Actions
|
|
6
|
+
module SharedValues
|
|
7
|
+
BETADROP_INSPECT_REPORT = :BETADROP_INSPECT_REPORT
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Check a build will install from a link without uploading it to the account — the step to put
|
|
11
|
+
# between gym and the rest of a release lane.
|
|
12
|
+
class BetadropInspectAction < Action
|
|
13
|
+
def self.run(params)
|
|
14
|
+
file = params[:file] || Actions.lane_context[SharedValues::IPA_OUTPUT_PATH] ||
|
|
15
|
+
Actions.lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH]
|
|
16
|
+
UI.user_error!('No build to inspect. Pass file:, or run gym / gradle first.') if file.to_s.empty?
|
|
17
|
+
|
|
18
|
+
report = Helper::BetadropHelper.new(api_token: params[:api_token], api_url: params[:api_url]).inspect_build(file)
|
|
19
|
+
Actions.lane_context[SharedValues::BETADROP_INSPECT_REPORT] = report
|
|
20
|
+
|
|
21
|
+
(report['checks'] || []).each do |check|
|
|
22
|
+
case check['level']
|
|
23
|
+
when 'error' then UI.error(check['message'])
|
|
24
|
+
when 'warning' then UI.important(check['message'])
|
|
25
|
+
else UI.success(check['message'])
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
failed = (report['checks'] || []).any? do |c|
|
|
30
|
+
c['level'] == 'error' || (params[:strict] && c['level'] == 'warning')
|
|
31
|
+
end
|
|
32
|
+
UI.user_error!("#{File.basename(file)} failed the install check.") if failed && params[:fail_on_error]
|
|
33
|
+
UI.success('Installable from a link.') unless failed
|
|
34
|
+
report
|
|
35
|
+
rescue Helper::BetadropHelper::Error => e
|
|
36
|
+
UI.user_error!(e.message)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.description
|
|
40
|
+
'Check an .ipa or .apk will install from a link: profile type and expiry, signing, testOnly'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.authors
|
|
44
|
+
['BetaDrop']
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.return_value
|
|
48
|
+
'The inspection report (a Hash)'
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.available_options
|
|
52
|
+
[
|
|
53
|
+
FastlaneCore::ConfigItem.new(key: :api_token, env_name: 'BETADROP_TOKEN', sensitive: true,
|
|
54
|
+
description: 'BetaDrop API token; a read-only token is enough', optional: false),
|
|
55
|
+
FastlaneCore::ConfigItem.new(key: :file, optional: true, description: 'Path to the .ipa or .apk'),
|
|
56
|
+
FastlaneCore::ConfigItem.new(key: :strict, optional: true, type: Boolean, default_value: false,
|
|
57
|
+
description: 'Treat warnings as failures'),
|
|
58
|
+
FastlaneCore::ConfigItem.new(key: :fail_on_error, optional: true, type: Boolean, default_value: true,
|
|
59
|
+
description: 'Fail the lane when a check fails'),
|
|
60
|
+
FastlaneCore::ConfigItem.new(key: :api_url, env_name: 'BETADROP_API_URL', optional: true,
|
|
61
|
+
default_value: 'https://api.betadrop.app', description: 'API host')
|
|
62
|
+
]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.is_supported?(platform)
|
|
66
|
+
%i[ios android].include?(platform)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.example_code
|
|
70
|
+
['betadrop_inspect', 'betadrop_inspect(strict: true)']
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.category
|
|
74
|
+
:beta
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'net/http'
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require 'stringio'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require_relative '../version'
|
|
7
|
+
|
|
8
|
+
module Fastlane
|
|
9
|
+
module Helper
|
|
10
|
+
# The HTTP half of the plugin, kept free of fastlane so it can be tested with plain Ruby.
|
|
11
|
+
#
|
|
12
|
+
# Uploads stream from disk: a build can be hundreds of megabytes, and reading it into memory
|
|
13
|
+
# to build a multipart body is how a CI runner with a small heap falls over.
|
|
14
|
+
class BetadropHelper
|
|
15
|
+
DEFAULT_API_URL = 'https://api.betadrop.app'.freeze
|
|
16
|
+
|
|
17
|
+
class Error < StandardError
|
|
18
|
+
attr_reader :status, :reason
|
|
19
|
+
|
|
20
|
+
def initialize(message, status: nil, reason: nil)
|
|
21
|
+
super(message)
|
|
22
|
+
@status = status
|
|
23
|
+
@reason = reason
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# An IO that reads several IOs one after another, so Net::HTTP can stream the multipart
|
|
28
|
+
# preamble, the file and the epilogue as one body with a known length.
|
|
29
|
+
class ChainIO
|
|
30
|
+
def initialize(*parts)
|
|
31
|
+
@parts = parts
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def read(length = nil, outbuf = nil)
|
|
35
|
+
outbuf ||= +''
|
|
36
|
+
outbuf.clear
|
|
37
|
+
until @parts.empty?
|
|
38
|
+
chunk = @parts.first.read(length ? length - outbuf.bytesize : nil)
|
|
39
|
+
if chunk.nil? || chunk.empty?
|
|
40
|
+
@parts.shift
|
|
41
|
+
next
|
|
42
|
+
end
|
|
43
|
+
outbuf << chunk
|
|
44
|
+
break if length && outbuf.bytesize >= length
|
|
45
|
+
end
|
|
46
|
+
outbuf.empty? && length ? nil : outbuf
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def initialize(api_token:, api_url: nil, user_agent: nil)
|
|
51
|
+
raise Error, 'A BetaDrop API token is required (api_token, or BETADROP_TOKEN).' if api_token.to_s.strip.empty?
|
|
52
|
+
|
|
53
|
+
@token = api_token.strip
|
|
54
|
+
@base = (api_url.to_s.strip.empty? ? DEFAULT_API_URL : api_url.strip).sub(%r{/+\z}, '')
|
|
55
|
+
@user_agent = user_agent || "fastlane-plugin-betadrop/#{Fastlane::Betadrop::VERSION}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# POST /api/cli/publish. `fields` values that are nil or empty are not sent.
|
|
59
|
+
def publish(file, fields = {})
|
|
60
|
+
multipart('/api/cli/publish', file, fields)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# POST /api/inspect. Stores nothing on the account.
|
|
64
|
+
def inspect_build(file)
|
|
65
|
+
multipart('/api/inspect', file, {})
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Turn the three limit options into the API's expiry fields — the same rule the CLI uses:
|
|
69
|
+
# one limit is that kind, more than one is `combined`, none sends nothing.
|
|
70
|
+
def self.expiry_fields(days: nil, downloads: nil, devices: nil, permanent: false)
|
|
71
|
+
return { 'expiry_type' => 'none' } if permanent
|
|
72
|
+
|
|
73
|
+
set = { 'expiry_time_days' => days, 'expiry_download_limit' => downloads, 'expiry_device_limit' => devices }
|
|
74
|
+
.reject { |_, v| v.nil? || v.to_s.strip.empty? }
|
|
75
|
+
return {} if set.empty?
|
|
76
|
+
|
|
77
|
+
type = if set.size > 1 then 'combined'
|
|
78
|
+
elsif set.key?('expiry_time_days') then 'time'
|
|
79
|
+
elsif set.key?('expiry_download_limit') then 'downloads'
|
|
80
|
+
else 'devices'
|
|
81
|
+
end
|
|
82
|
+
set.merge('expiry_type' => type)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def multipart(path, file, fields)
|
|
88
|
+
raise Error, "No build found at #{file}" unless file && File.file?(file)
|
|
89
|
+
|
|
90
|
+
boundary = "----betadropfastlane#{SecureRandom.hex(16)}"
|
|
91
|
+
preamble = +''
|
|
92
|
+
fields.each do |key, value|
|
|
93
|
+
next if value.nil? || value.to_s.empty?
|
|
94
|
+
|
|
95
|
+
preamble << "--#{boundary}\r\nContent-Disposition: form-data; name=\"#{key}\"\r\n\r\n#{value}\r\n"
|
|
96
|
+
end
|
|
97
|
+
filename = File.basename(file).gsub(/["\\\r\n]/, '_')
|
|
98
|
+
preamble << "--#{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"#{filename}\"\r\n" \
|
|
99
|
+
"Content-Type: application/octet-stream\r\n\r\n"
|
|
100
|
+
epilogue = "\r\n--#{boundary}--\r\n"
|
|
101
|
+
|
|
102
|
+
uri = URI("#{@base}#{path}")
|
|
103
|
+
File.open(file, 'rb') do |io|
|
|
104
|
+
request = Net::HTTP::Post.new(uri)
|
|
105
|
+
request['Authorization'] = "Bearer #{@token}"
|
|
106
|
+
request['Accept'] = 'application/json'
|
|
107
|
+
request['User-Agent'] = @user_agent
|
|
108
|
+
request['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
|
|
109
|
+
request['Content-Length'] = (preamble.bytesize + File.size(file) + epilogue.bytesize).to_s
|
|
110
|
+
request.body_stream = ChainIO.new(StringIO.new(preamble), io, StringIO.new(epilogue))
|
|
111
|
+
send_request(uri, request)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def send_request(uri, request)
|
|
116
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
117
|
+
http.use_ssl = uri.scheme == 'https'
|
|
118
|
+
http.open_timeout = 30
|
|
119
|
+
# A large upload on a slow runner. The server answers once the file is stored and parsed.
|
|
120
|
+
http.read_timeout = 900
|
|
121
|
+
http.write_timeout = 900 if http.respond_to?(:write_timeout=)
|
|
122
|
+
response = http.request(request)
|
|
123
|
+
body = begin
|
|
124
|
+
JSON.parse(response.body.to_s)
|
|
125
|
+
rescue JSON::ParserError
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
status = response.code.to_i
|
|
129
|
+
if status == 401
|
|
130
|
+
raise Error.new('BetaDrop rejected the API token: it is invalid, expired or revoked.', status: status)
|
|
131
|
+
end
|
|
132
|
+
if body.nil?
|
|
133
|
+
raise Error.new("Unexpected response from BetaDrop (HTTP #{status}).", status: status)
|
|
134
|
+
end
|
|
135
|
+
unless status.between?(200, 299) && body['success'] != false
|
|
136
|
+
raise Error.new(body['error'] || body['message'] || "Request failed (HTTP #{status}).",
|
|
137
|
+
status: status, reason: body['reason'])
|
|
138
|
+
end
|
|
139
|
+
body['data']
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require 'fastlane/plugin/betadrop/version'
|
|
2
|
+
|
|
3
|
+
module Fastlane
|
|
4
|
+
module Betadrop
|
|
5
|
+
# Loads every action and helper in this plugin, the way fastlane's plugin loader expects.
|
|
6
|
+
def self.all_classes
|
|
7
|
+
Dir[File.expand_path('**/{actions,helper}/*.rb', File.dirname(__FILE__))]
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
Fastlane::Betadrop.all_classes.each do |current|
|
|
13
|
+
require current
|
|
14
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fastlane-plugin-betadrop
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- BetaDrop
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-11-15 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Publishes the .ipa or .apk your lane built to BetaDrop, optionally checks
|
|
14
|
+
it will install from a link first, and can point a standing link at it.
|
|
15
|
+
email: support@betadrop.app
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- LICENSE
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/fastlane/plugin/betadrop.rb
|
|
23
|
+
- lib/fastlane/plugin/betadrop/actions/betadrop_action.rb
|
|
24
|
+
- lib/fastlane/plugin/betadrop/actions/betadrop_inspect_action.rb
|
|
25
|
+
- lib/fastlane/plugin/betadrop/helper/betadrop_helper.rb
|
|
26
|
+
- lib/fastlane/plugin/betadrop/version.rb
|
|
27
|
+
homepage: https://betadrop.app/integrations/fastlane/
|
|
28
|
+
licenses:
|
|
29
|
+
- MIT
|
|
30
|
+
metadata:
|
|
31
|
+
source_code_uri: https://github.com/betadrop-app/fastlane-plugin-betadrop
|
|
32
|
+
documentation_uri: https://betadrop.app/integrations/fastlane/
|
|
33
|
+
post_install_message:
|
|
34
|
+
rdoc_options: []
|
|
35
|
+
require_paths:
|
|
36
|
+
- lib
|
|
37
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - ">="
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '2.6'
|
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
requirements: []
|
|
48
|
+
rubygems_version: 3.0.3.1
|
|
49
|
+
signing_key:
|
|
50
|
+
specification_version: 4
|
|
51
|
+
summary: Upload iOS and Android builds to BetaDrop and get an over-the-air install
|
|
52
|
+
link.
|
|
53
|
+
test_files: []
|