fastlane-plugin-appcircle_testing_distribution 0.4.0 → 0.4.2
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 +4 -4
- data/README.md +13 -2
- data/lib/fastlane/plugin/appcircle_testing_distribution/actions/appcircle_testing_distribution_action.rb +47 -20
- data/lib/fastlane/plugin/appcircle_testing_distribution/helper/TDAuthService.rb +36 -4
- data/lib/fastlane/plugin/appcircle_testing_distribution/helper/TDUploadService.rb +70 -10
- data/lib/fastlane/plugin/appcircle_testing_distribution/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 213b75b8d9c150a9206153fa4bb835277992aac5ba70544ac201337a53eae712
|
|
4
|
+
data.tar.gz: 6dc6d2d960d77b3b7db6976b58aeefc17e0cfb3164bc52d137779a2f7765ae36
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3ec4a590c5418d685a2f2c7fa9b2618bf09d91d3a6bcc26252d1ec6e164b4d39a756065f0b35d886f79e75e04c0ea40a797c9de9c373ab35c9b3e3b920fdbabe
|
|
7
|
+
data.tar.gz: 0e980015ac5ee5f865da4262a1d7bd090f2c2d5cb8ce4e761ff8e8e718d61ea8822bb8c47295f1f45342d28bfaa2204923dcea78853b081a013ab21fca6aece7
|
data/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Testing distribution is the process of distributing test builds to designated te
|
|
|
13
13
|
## Benefits of Using Testing Distribution
|
|
14
14
|
|
|
15
15
|
1. **Simplified Binary Distribution**.
|
|
16
|
-
- **Skip Traditional Stores:** Share
|
|
16
|
+
- **Skip Traditional Stores:** Share IPA, APK, AAB files directly, avoiding the need to use App Store TestFlight or Google Play Internal Testing.
|
|
17
17
|
2. **Streamlined Workflow:**
|
|
18
18
|
- **Automated Processes:** Platforms like Appcircle automate the distribution process, saving time and reducing manual effort.
|
|
19
19
|
- **Seamless Integration:** Integrates smoothly with existing DevOps pipelines, enabling efficient build and distribution workflows.
|
|
@@ -67,9 +67,10 @@ fastlane add_plugin appcircle_testing_distribution
|
|
|
67
67
|
```ruby
|
|
68
68
|
appcircle_testing_distribution(
|
|
69
69
|
personalAPIToken: ENV["AC_PERSONAL_API_TOKEN"],
|
|
70
|
+
personalAccessKey: ENV["AC_PERSONAL_ACCESS_KEY"],
|
|
70
71
|
subOrganizationName: ENV["AC_SUB_ORGANIZATION_NAME"],
|
|
71
72
|
profileName: ENV["AC_PROFILE_NAME"],
|
|
72
|
-
createProfileIfNotExists: ENV["AC_CREATE_PROFILE_IF_NOT_EXISTS"],
|
|
73
|
+
createProfileIfNotExists: ENV["AC_CREATE_PROFILE_IF_NOT_EXISTS"] == "true",
|
|
73
74
|
profileCreationSettings: {
|
|
74
75
|
authType: ENV["AC_PROFILE_AUTH_TYPE"],
|
|
75
76
|
username: ENV["AC_PROFILE_USERNAME"],
|
|
@@ -81,7 +82,17 @@ fastlane add_plugin appcircle_testing_distribution
|
|
|
81
82
|
)
|
|
82
83
|
```
|
|
83
84
|
|
|
85
|
+
### Authentication
|
|
86
|
+
|
|
87
|
+
Provide **either** `personalAPIToken` **or** `personalAccessKey` — not both. If neither is provided, or both are provided at the same time, the plugin fails fast with a descriptive error.
|
|
88
|
+
|
|
84
89
|
- `personalAPIToken`: The Appcircle Personal API token used to authenticate and authorize access to Appcircle services within this plugin.
|
|
90
|
+
- `personalAccessKey`: Alternative authentication method using a Personal Access Key. Use this if your organization provisions access keys instead of API tokens.
|
|
91
|
+
|
|
92
|
+
> **Note:** `subOrganizationName` is currently supported only when authenticating with `personalAPIToken`. When used together with `personalAccessKey`, the sub-organization switch is skipped with a warning.
|
|
93
|
+
|
|
94
|
+
### Other parameters
|
|
95
|
+
|
|
85
96
|
- `subOrganizationName` (optional): Required when the Root Organization's `personalAPIToken` is used, and you want to create the profile under a sub-organization. In this case, provide the name of the sub-organization in this field. If you directly used the sub-organization's `personalAPIToken`, this parameter is not needed.
|
|
86
97
|
- `profileName`: Specifies the profile that will be used for uploading the app.
|
|
87
98
|
- `createProfileIfNotExists` (optional): Ensures that a testing distribution profile is automatically created if it does not already exist; if the profile name already exists, the app will be uploaded to that existing profile instead.
|
|
@@ -10,7 +10,7 @@ require_relative '../helper/TDUploadService'
|
|
|
10
10
|
module Fastlane
|
|
11
11
|
module Actions
|
|
12
12
|
class AppcircleTestingDistributionAction < Action
|
|
13
|
-
VALID_EXTENSIONS = ['.apk', '.aab', '.ipa'
|
|
13
|
+
VALID_EXTENSIONS = ['.apk', '.aab', '.ipa']
|
|
14
14
|
AUTH_TYPE_MAPPING = {
|
|
15
15
|
'none' => 1, # None
|
|
16
16
|
'static' => 3, # Static Username and Password
|
|
@@ -20,6 +20,7 @@ module Fastlane
|
|
|
20
20
|
|
|
21
21
|
def self.run(params)
|
|
22
22
|
personalAPIToken = params[:personalAPIToken]
|
|
23
|
+
personalAccessKey = params[:personalAccessKey]
|
|
23
24
|
subOrganizationName = params[:subOrganizationName]
|
|
24
25
|
profileName = params[:profileName]
|
|
25
26
|
createProfileIfNotExists = params[:createProfileIfNotExists] || false
|
|
@@ -32,11 +33,20 @@ module Fastlane
|
|
|
32
33
|
#
|
|
33
34
|
appPath = params[:appPath]
|
|
34
35
|
message = params[:message]
|
|
35
|
-
|
|
36
|
+
|
|
36
37
|
profileAuthType = AUTH_TYPE_MAPPING[profileAuthType] # map input to API values
|
|
37
38
|
|
|
39
|
+
# Validate auth input (either-or, not both, not none)
|
|
40
|
+
if personalAPIToken.nil? && personalAccessKey.nil?
|
|
41
|
+
UI.user_error!("Either Personal API Token or Personal Access Key is required to authenticate connections to Appcircle services. Please provide a valid access token or access key.")
|
|
42
|
+
elsif !personalAPIToken.nil? && !personalAccessKey.nil?
|
|
43
|
+
UI.user_error!("Personal API Token and Personal Access Key cannot be used together. Please provide only one authentication method.")
|
|
44
|
+
end
|
|
45
|
+
|
|
38
46
|
# Auth
|
|
39
|
-
authToken = self.ac_login(personalAPIToken,
|
|
47
|
+
authToken = self.ac_login(personal_api_token: personalAPIToken,
|
|
48
|
+
personal_access_key: personalAccessKey,
|
|
49
|
+
sub_organization_name: subOrganizationName)
|
|
40
50
|
|
|
41
51
|
# Get or create profile
|
|
42
52
|
profileId = self.ac_get_or_create_profile(authToken, profileName, createProfileIfNotExists, profileCreationSettings, profileAuthType, profileUsername, profilePassword, profileTestingGroupNames)
|
|
@@ -45,21 +55,29 @@ module Fastlane
|
|
|
45
55
|
self.ac_upload(authToken, appPath, profileId, profileName, message)
|
|
46
56
|
end
|
|
47
57
|
|
|
48
|
-
def self.ac_login(
|
|
58
|
+
def self.ac_login(personal_api_token:, personal_access_key:, sub_organization_name:)
|
|
49
59
|
begin
|
|
50
60
|
token = ''
|
|
51
61
|
|
|
52
|
-
|
|
62
|
+
if personal_access_key
|
|
63
|
+
user = TDAuthService.get_ac_token_with_personal_access_key(personal_access_key: personal_access_key)
|
|
64
|
+
else
|
|
65
|
+
user = TDAuthService.get_ac_token(pat: personal_api_token)
|
|
66
|
+
end
|
|
53
67
|
UI.success("Login is successful.")
|
|
54
68
|
token = user.accessToken
|
|
55
|
-
|
|
56
|
-
if
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
|
|
70
|
+
if sub_organization_name
|
|
71
|
+
if personal_access_key
|
|
72
|
+
UI.important("Warning: subOrganizationName is currently only supported with personalAPIToken auth. Ignoring sub-organization switch for Personal Access Key login.")
|
|
73
|
+
else
|
|
74
|
+
organization_id = TDAuthService.get_organization_id(access_token: token, name: sub_organization_name)
|
|
75
|
+
user = TDAuthService.get_ac_token(pat: personal_api_token, sub_organization_id: organization_id)
|
|
76
|
+
UI.message("Switched to sub-organization: #{sub_organization_name}")
|
|
77
|
+
token = user.accessToken
|
|
78
|
+
end
|
|
61
79
|
end
|
|
62
|
-
|
|
80
|
+
|
|
63
81
|
return token
|
|
64
82
|
|
|
65
83
|
rescue => e
|
|
@@ -162,19 +180,25 @@ module Fastlane
|
|
|
162
180
|
def self.available_options
|
|
163
181
|
[
|
|
164
182
|
FastlaneCore::ConfigItem.new(key: :personalAPIToken,
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
183
|
+
env_name: "AC_PERSONAL_API_TOKEN",
|
|
184
|
+
description: "Provide Personal API Token to authenticate connections to Appcircle services (alternative to personalAccessKey)",
|
|
185
|
+
optional: true,
|
|
186
|
+
type: String),
|
|
187
|
+
|
|
188
|
+
FastlaneCore::ConfigItem.new(key: :personalAccessKey,
|
|
189
|
+
env_name: "AC_PERSONAL_ACCESS_KEY",
|
|
190
|
+
description: "Provide Personal Access Key to authenticate connections to Appcircle services (alternative to personalAPIToken)",
|
|
191
|
+
optional: true,
|
|
192
|
+
type: String),
|
|
171
193
|
|
|
172
194
|
FastlaneCore::ConfigItem.new(key: :subOrganizationName,
|
|
195
|
+
env_name: "AC_SUB_ORGANIZATION_NAME",
|
|
173
196
|
description: "Optional: Sub-organization name for app distribution. Profiles will be created under root organization if not provided",
|
|
174
197
|
optional: true,
|
|
175
198
|
type: String),
|
|
176
199
|
|
|
177
200
|
FastlaneCore::ConfigItem.new(key: :profileName,
|
|
201
|
+
env_name: "AC_PROFILE_NAME",
|
|
178
202
|
description: "Enter the profile name of the Appcircle testing distribution profile. This name uniquely identifies the profile under which your applications will be distributed",
|
|
179
203
|
optional: false,
|
|
180
204
|
type: String,
|
|
@@ -183,6 +207,7 @@ module Fastlane
|
|
|
183
207
|
end),
|
|
184
208
|
|
|
185
209
|
FastlaneCore::ConfigItem.new(key: :createProfileIfNotExists,
|
|
210
|
+
env_name: "AC_CREATE_PROFILE_IF_NOT_EXISTS",
|
|
186
211
|
description: "Optional: If the profile does not exist, create a new profile with the given name",
|
|
187
212
|
optional: true,
|
|
188
213
|
type: Boolean),
|
|
@@ -212,7 +237,8 @@ module Fastlane
|
|
|
212
237
|
end),
|
|
213
238
|
|
|
214
239
|
FastlaneCore::ConfigItem.new(key: :appPath,
|
|
215
|
-
|
|
240
|
+
env_name: "AC_APP_PATH",
|
|
241
|
+
description: "Specify the path to your application file. For iOS, this can be a .ipa file path. For Android, specify the .apk or .aab file path",
|
|
216
242
|
optional: false,
|
|
217
243
|
type: String,
|
|
218
244
|
verify_block: proc do |value|
|
|
@@ -220,11 +246,12 @@ module Fastlane
|
|
|
220
246
|
|
|
221
247
|
file_extension = File.extname(value).downcase
|
|
222
248
|
unless VALID_EXTENSIONS.include?(file_extension)
|
|
223
|
-
UI.user_error!("Invalid file extension: '#{file_extension}'. For Android, use .apk or .aab. For iOS, use .ipa
|
|
249
|
+
UI.user_error!("Invalid file extension: '#{file_extension}'. For Android, use .apk or .aab. For iOS, use .ipa.")
|
|
224
250
|
end
|
|
225
251
|
end),
|
|
226
252
|
|
|
227
253
|
FastlaneCore::ConfigItem.new(key: :message,
|
|
254
|
+
env_name: "AC_MESSAGE",
|
|
228
255
|
description: "Message to include with the distribution to provide additional information to testers or users receiving the build",
|
|
229
256
|
optional: false,
|
|
230
257
|
type: String,
|
|
@@ -49,23 +49,23 @@ module TDAuthService
|
|
|
49
49
|
def self.get_organization_id(access_token:, name:)
|
|
50
50
|
endpoint_url = 'https://api.appcircle.io/identity/v1/organizations'
|
|
51
51
|
uri = URI(endpoint_url)
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
# Create HTTP request
|
|
54
54
|
request = Net::HTTP::Get.new(uri)
|
|
55
55
|
request['Authorization'] = "Bearer #{access_token}"
|
|
56
56
|
request['Accept'] = 'application/json'
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
# Make the HTTP request
|
|
59
59
|
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
60
60
|
http.request(request)
|
|
61
61
|
end
|
|
62
|
-
|
|
62
|
+
|
|
63
63
|
# Check response
|
|
64
64
|
if response.is_a?(Net::HTTPSuccess)
|
|
65
65
|
response_data = JSON.parse(response.body)
|
|
66
66
|
organizations = response_data['data']
|
|
67
67
|
organization = organizations.find { |org| org['name'] == name }
|
|
68
|
-
|
|
68
|
+
|
|
69
69
|
raise "Organization with name '#{name}' not found" unless organization
|
|
70
70
|
return organization['id']
|
|
71
71
|
|
|
@@ -73,4 +73,36 @@ module TDAuthService
|
|
|
73
73
|
raise "Error: (#{response.code} #{response.message})"
|
|
74
74
|
end
|
|
75
75
|
end
|
|
76
|
+
|
|
77
|
+
def self.get_ac_token_with_personal_access_key(personal_access_key:)
|
|
78
|
+
endpoint_url = 'https://auth.appcircle.io/auth/v1/token'
|
|
79
|
+
uri = URI(endpoint_url)
|
|
80
|
+
|
|
81
|
+
# Create HTTP request
|
|
82
|
+
request = Net::HTTP::Post.new(uri)
|
|
83
|
+
request.content_type = 'application/x-www-form-urlencoded'
|
|
84
|
+
request['Accept'] = 'application/json'
|
|
85
|
+
|
|
86
|
+
# Encode parameters
|
|
87
|
+
params = { 'personal-access-key' => personal_access_key }
|
|
88
|
+
request.body = URI.encode_www_form(params)
|
|
89
|
+
|
|
90
|
+
# Make the HTTP request
|
|
91
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
92
|
+
http.request(request)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Check response
|
|
96
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
97
|
+
response_data = JSON.parse(response.body)
|
|
98
|
+
|
|
99
|
+
user = UserResponse.new(
|
|
100
|
+
accessToken: response_data['access_token']
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return user
|
|
104
|
+
else
|
|
105
|
+
raise "Error: (#{response.code} #{response.message})."
|
|
106
|
+
end
|
|
107
|
+
end
|
|
76
108
|
end
|
|
@@ -6,20 +6,80 @@ require 'rest-client'
|
|
|
6
6
|
BASE_URL = "https://api.appcircle.io"
|
|
7
7
|
|
|
8
8
|
module TDUploadService
|
|
9
|
+
UI = FastlaneCore::UI
|
|
10
|
+
|
|
9
11
|
def self.upload_artifact(token:, message:, app:, dist_profile_id:)
|
|
10
|
-
|
|
12
|
+
file_path = app
|
|
13
|
+
file_name = File.basename(file_path)
|
|
14
|
+
file_size = File.size(file_path)
|
|
15
|
+
|
|
16
|
+
upload_info_url = "#{BASE_URL}/distribution/v1/profiles/#{dist_profile_id}/app-versions"
|
|
11
17
|
headers = {
|
|
12
|
-
Authorization: "Bearer #{token}"
|
|
13
|
-
|
|
14
|
-
payload = {
|
|
15
|
-
Message: message,
|
|
16
|
-
File: File.new(app, 'rb'),
|
|
17
|
-
multipart: true # Force multipart encoding for RestClient
|
|
18
|
+
Authorization: "Bearer #{token}",
|
|
19
|
+
accept: 'application/json'
|
|
18
20
|
}
|
|
19
|
-
|
|
21
|
+
|
|
22
|
+
uri = URI(upload_info_url)
|
|
23
|
+
uri.query = URI.encode_www_form({
|
|
24
|
+
action: 'uploadInformation',
|
|
25
|
+
fileName: file_name,
|
|
26
|
+
fileSize: file_size
|
|
27
|
+
})
|
|
28
|
+
|
|
20
29
|
begin
|
|
21
|
-
|
|
22
|
-
|
|
30
|
+
UI.message("Getting file upload information...")
|
|
31
|
+
response = RestClient.get(uri.to_s, headers)
|
|
32
|
+
upload_info = JSON.parse(response.body)
|
|
33
|
+
if response.code.between?(200, 299)
|
|
34
|
+
UI.success("File upload information retrieved successfully with status code: #{response.code}")
|
|
35
|
+
else
|
|
36
|
+
UI.error("Failed to retrieve file upload information with status code: #{response.code}")
|
|
37
|
+
raise "Failed to retrieve file upload information."
|
|
38
|
+
end
|
|
39
|
+
file_id = upload_info['fileId']
|
|
40
|
+
upload_url = upload_info['uploadUrl']
|
|
41
|
+
|
|
42
|
+
file_content = File.binread(file_path)
|
|
43
|
+
UI.message("Uploading file to Appcircle...")
|
|
44
|
+
response = RestClient.put(
|
|
45
|
+
upload_url,
|
|
46
|
+
file_content,
|
|
47
|
+
{ content_type: 'application/octet-stream' }
|
|
48
|
+
)
|
|
49
|
+
if response.code.between?(200, 299)
|
|
50
|
+
UI.success("File upload finished successfully with status code: #{response.code}")
|
|
51
|
+
else
|
|
52
|
+
UI.error("File upload failed with status code: #{response.code}")
|
|
53
|
+
raise "File upload failed."
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
commit_url = "#{BASE_URL}/distribution/v1/profiles/#{dist_profile_id}/app-versions"
|
|
57
|
+
uri = URI(commit_url)
|
|
58
|
+
uri.query = URI.encode_www_form({ action: 'commitFileUpload' })
|
|
59
|
+
|
|
60
|
+
commit_payload = {
|
|
61
|
+
fileId: file_id,
|
|
62
|
+
fileName: file_name,
|
|
63
|
+
message: message
|
|
64
|
+
}.to_json
|
|
65
|
+
|
|
66
|
+
commit_headers = {
|
|
67
|
+
Authorization: "Bearer #{token}",
|
|
68
|
+
content_type: :json,
|
|
69
|
+
accept: 'application/json'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
UI.message("Committing file upload...")
|
|
73
|
+
commit_response = RestClient.post(uri.to_s, commit_payload, commit_headers)
|
|
74
|
+
if commit_response.code.between?(200, 299)
|
|
75
|
+
result = JSON.parse(commit_response.body)
|
|
76
|
+
UI.success("Commit successful with status code: #{commit_response.code}")
|
|
77
|
+
else
|
|
78
|
+
UI.error("Commit failed with status code: #{commit_response.code}")
|
|
79
|
+
raise "Commit failed with status code: #{commit_response.code}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
return result
|
|
23
83
|
rescue RestClient::ExceptionWithResponse => e
|
|
24
84
|
raise e
|
|
25
85
|
rescue StandardError => e
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fastlane-plugin-appcircle_testing_distribution
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- appcircleio
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-04-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rest-client
|
|
@@ -59,7 +59,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
60
|
version: '0'
|
|
61
61
|
requirements: []
|
|
62
|
-
rubygems_version: 3.4.
|
|
62
|
+
rubygems_version: 3.4.10
|
|
63
63
|
signing_key:
|
|
64
64
|
specification_version: 4
|
|
65
65
|
summary: Efficiently distribute application builds to users or testing groups using
|