fastlane-plugin-appcircle_testing_distribution 0.4.3.beta.2 → 0.5.0.beta.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f283cd5be2e98b217e7d2e42850522554fed9558546b93b866cc770e129f8d54
4
- data.tar.gz: 56007872a9fa3cd4622aee13c333caed3ac64b40567daef6acfc68df3a4080ec
3
+ metadata.gz: 83c9d8655326e41c07f7b6d927fb63441c01059e2c29c2658338931c5a2ae718
4
+ data.tar.gz: fc194ce33b458182af4725f5e21b525666c9cab85cc41b9c5a2d29444251488d
5
5
  SHA512:
6
- metadata.gz: 32aed19f76d5041ae3e4e0347b643511a3f0208be7c7ccfbd0beab39aaf4f39a585005c518e99cb8b440962e263d4a37c4f4c5a4308e14689dc4bd39ce50d346
7
- data.tar.gz: 99d4e56d0378881862c88fab9dc1a47b5fa3337005b7837e6440b86bdb72625040f0cd5909bab4550068c1b4d67b9a36700996e6adb9b9b6a639e2655912d924
6
+ metadata.gz: 2a0568378f254b846f291bc6dc9bb0b149b8590f8945fa521f91d670eca5bca37818af392cf9028cc3580ff6bb92ad6cb07a5c606e016b7a0da07fac215bf1a4
7
+ data.tar.gz: 4a64eab748b2a2163b464ac87793982687940e8c9df4cb1ca8ca7501f42e7d91472b91c66eb627b0fa07fb7bb2b8bb7f77bae07608bb6b866d04737f7defda10
data/README.md CHANGED
@@ -104,6 +104,24 @@ Provide **either** `personalAPIToken` **or** `personalAccessKey` — not both. I
104
104
  - `appPath`: Indicates the file path to the application package that will be uploaded to Appcircle Testing Distribution Profile.
105
105
  - `message`: Your message to testers, ensuring they receive important updates and information regarding the application.
106
106
 
107
+ ### Self-Hosted Appcircle
108
+
109
+ If you run a self-hosted Appcircle installation, point the plugin to your own endpoints. Both parameters are optional and default to the Appcircle cloud, so existing cloud users do not need to set them.
110
+
111
+ - `authEndpoint` (optional): Authentication endpoint URL. Defaults to `https://auth.appcircle.io`.
112
+ - `apiEndpoint` (optional): API endpoint URL. Defaults to `https://api.appcircle.io`.
113
+
114
+ ```ruby
115
+ appcircle_testing_distribution(
116
+ personalAPIToken: ENV["AC_PERSONAL_API_TOKEN"],
117
+ authEndpoint: "https://auth.my-appcircle.example.com",
118
+ apiEndpoint: "https://api.my-appcircle.example.com",
119
+ profileName: ENV["AC_PROFILE_NAME"],
120
+ appPath: ENV["AC_APP_PATH"],
121
+ message: ENV["AC_MESSAGE"]
122
+ )
123
+ ```
124
+
107
125
  ## Further Details
108
126
 
109
127
  For more information please refer to the documentation.
@@ -33,6 +33,9 @@ module Fastlane
33
33
  #
34
34
  appPath = params[:appPath]
35
35
  message = params[:message]
36
+ #
37
+ authEndpoint = params[:authEndpoint]
38
+ apiEndpoint = params[:apiEndpoint]
36
39
 
37
40
  profileAuthType = AUTH_TYPE_MAPPING[profileAuthType] # map input to API values
38
41
 
@@ -46,23 +49,25 @@ module Fastlane
46
49
  # Auth
47
50
  authToken = self.ac_login(personal_api_token: personalAPIToken,
48
51
  personal_access_key: personalAccessKey,
49
- sub_organization_name: subOrganizationName)
52
+ sub_organization_name: subOrganizationName,
53
+ auth_endpoint: authEndpoint,
54
+ api_endpoint: apiEndpoint)
50
55
 
51
56
  # Get or create profile
52
- profileId = self.ac_get_or_create_profile(authToken, profileName, createProfileIfNotExists, profileCreationSettings, profileAuthType, profileUsername, profilePassword, profileTestingGroupNames)
57
+ profileId = self.ac_get_or_create_profile(authToken, profileName, createProfileIfNotExists, profileCreationSettings, profileAuthType, profileUsername, profilePassword, profileTestingGroupNames, apiEndpoint)
53
58
 
54
59
  # Upload package
55
- self.ac_upload(authToken, appPath, profileId, profileName, message)
60
+ self.ac_upload(authToken, appPath, profileId, profileName, message, apiEndpoint)
56
61
  end
57
62
 
58
- def self.ac_login(personal_api_token:, personal_access_key:, sub_organization_name:)
63
+ def self.ac_login(personal_api_token:, personal_access_key:, sub_organization_name:, auth_endpoint:, api_endpoint:)
59
64
  begin
60
65
  token = ''
61
66
 
62
67
  if personal_access_key
63
- user = TDAuthService.get_ac_token_with_personal_access_key(personal_access_key: personal_access_key)
68
+ user = TDAuthService.get_ac_token_with_personal_access_key(personal_access_key: personal_access_key, auth_endpoint: auth_endpoint)
64
69
  else
65
- user = TDAuthService.get_ac_token(pat: personal_api_token)
70
+ user = TDAuthService.get_ac_token(pat: personal_api_token, auth_endpoint: auth_endpoint)
66
71
  end
67
72
  UI.success("Login is successful.")
68
73
  token = user.accessToken
@@ -71,8 +76,8 @@ module Fastlane
71
76
  if personal_access_key
72
77
  UI.important("Warning: subOrganizationName is currently only supported with personalAPIToken auth. Ignoring sub-organization switch for Personal Access Key login.")
73
78
  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)
79
+ organization_id = TDAuthService.get_organization_id(access_token: token, name: sub_organization_name, api_endpoint: api_endpoint)
80
+ user = TDAuthService.get_ac_token(pat: personal_api_token, sub_organization_id: organization_id, auth_endpoint: auth_endpoint)
76
81
  UI.message("Switched to sub-organization: #{sub_organization_name}")
77
82
  token = user.accessToken
78
83
  end
@@ -85,9 +90,9 @@ module Fastlane
85
90
  end
86
91
  end
87
92
 
88
- def self.ac_get_or_create_profile(authToken, profileName, createProfileIfNotExists, profileCreationSettings, profileAuthType, profileUsername, profilePassword, profileTestingGroupNames)
93
+ def self.ac_get_or_create_profile(authToken, profileName, createProfileIfNotExists, profileCreationSettings, profileAuthType, profileUsername, profilePassword, profileTestingGroupNames, apiEndpoint)
89
94
  begin
90
- profileId = TDUploadService.get_profile_id(authToken, profileName)
95
+ profileId = TDUploadService.get_profile_id(authToken, profileName, apiEndpoint)
91
96
 
92
97
  if profileId
93
98
  UI.message("Profile '#{profileName}' found with ID: #{profileId}.")
@@ -97,7 +102,7 @@ module Fastlane
97
102
  UI.user_error!("Error: Profile '#{profileName}' not found. The option 'createProfileIfNotExists' is set to false, so a new profile was not created. To automatically create a new profile when it doesn't exist, set 'createProfileIfNotExists' to true.")
98
103
  elsif profileId.nil? && createProfileIfNotExists
99
104
  UI.message("Profile '#{profileName}' not found. Creating the new profile...")
100
- profileId = TDUploadService.create_profile(authToken, profileName, profileAuthType, profileUsername, profilePassword, profileTestingGroupNames)
105
+ profileId = TDUploadService.create_profile(authToken, profileName, profileAuthType, profileUsername, profilePassword, profileTestingGroupNames, apiEndpoint)
101
106
  end
102
107
 
103
108
  return profileId
@@ -107,11 +112,11 @@ module Fastlane
107
112
  end
108
113
  end
109
114
 
110
- def self.ac_upload(token, appPath, profileID, profileName, message)
115
+ def self.ac_upload(token, appPath, profileID, profileName, message, apiEndpoint)
111
116
  begin
112
117
  UI.message("Upload started.")
113
- response = TDUploadService.upload_artifact(token: token, message: message, app: appPath, dist_profile_id: profileID)
114
- result = self.checkTaskStatus(token, response['taskId'])
118
+ response = TDUploadService.upload_artifact(token: token, message: message, app: appPath, dist_profile_id: profileID, api_endpoint: apiEndpoint)
119
+ result = self.checkTaskStatus(token, response['taskId'], apiEndpoint)
115
120
 
116
121
  if result
117
122
  UI.success("#{appPath} uploaded to profile '#{profileName}' successfully 🎉")
@@ -122,8 +127,8 @@ module Fastlane
122
127
  end
123
128
  end
124
129
 
125
- def self.checkTaskStatus(authToken, taskId)
126
- uri = URI.parse("https://api.appcircle.io/task/v1/tasks/#{taskId}")
130
+ def self.checkTaskStatus(authToken, taskId, apiEndpoint)
131
+ uri = URI.parse("#{apiEndpoint}/task/v1/tasks/#{taskId}")
127
132
 
128
133
  check_interval = 1
129
134
  # timeout = 2 * 60 * 60 # 2 hours in seconds
@@ -197,6 +202,20 @@ module Fastlane
197
202
  optional: true,
198
203
  type: String),
199
204
 
205
+ FastlaneCore::ConfigItem.new(key: :authEndpoint,
206
+ env_name: "AC_AUTH_ENDPOINT",
207
+ description: "Optional: Authentication endpoint URL for self-hosted Appcircle installations. Defaults to the Appcircle cloud",
208
+ optional: true,
209
+ default_value: "https://auth.appcircle.io",
210
+ type: String),
211
+
212
+ FastlaneCore::ConfigItem.new(key: :apiEndpoint,
213
+ env_name: "AC_API_ENDPOINT",
214
+ description: "Optional: API endpoint URL for self-hosted Appcircle installations. Defaults to the Appcircle cloud",
215
+ optional: true,
216
+ default_value: "https://api.appcircle.io",
217
+ type: String),
218
+
200
219
  FastlaneCore::ConfigItem.new(key: :profileName,
201
220
  env_name: "AC_PROFILE_NAME",
202
221
  description: "Enter the profile name of the Appcircle testing distribution profile. This name uniquely identifies the profile under which your applications will be distributed",
@@ -13,8 +13,8 @@ class UserResponse
13
13
  end
14
14
 
15
15
  module TDAuthService
16
- def self.get_ac_token(pat:, sub_organization_id: nil)
17
- endpoint_url = 'https://auth.appcircle.io/auth/v2/token'
16
+ def self.get_ac_token(pat:, sub_organization_id: nil, auth_endpoint: 'https://auth.appcircle.io')
17
+ endpoint_url = "#{auth_endpoint}/auth/v2/token"
18
18
  uri = URI(endpoint_url)
19
19
 
20
20
  # Create HTTP request
@@ -46,8 +46,8 @@ module TDAuthService
46
46
  end
47
47
  end
48
48
 
49
- def self.get_organization_id(access_token:, name:)
50
- endpoint_url = 'https://api.appcircle.io/identity/v1/organizations'
49
+ def self.get_organization_id(access_token:, name:, api_endpoint: 'https://api.appcircle.io')
50
+ endpoint_url = "#{api_endpoint}/identity/v1/organizations"
51
51
  uri = URI(endpoint_url)
52
52
 
53
53
  # Create HTTP request
@@ -74,8 +74,8 @@ module TDAuthService
74
74
  end
75
75
  end
76
76
 
77
- def self.get_ac_token_with_personal_access_key(personal_access_key:)
78
- endpoint_url = 'https://auth.appcircle.io/auth/v1/token'
77
+ def self.get_ac_token_with_personal_access_key(personal_access_key:, auth_endpoint: 'https://auth.appcircle.io')
78
+ endpoint_url = "#{auth_endpoint}/auth/v1/token"
79
79
  uri = URI(endpoint_url)
80
80
 
81
81
  # Create HTTP request
@@ -8,12 +8,35 @@ BASE_URL = "https://api.appcircle.io"
8
8
  module TDUploadService
9
9
  UI = FastlaneCore::UI
10
10
 
11
- def self.upload_artifact(token:, message:, app:, dist_profile_id:)
11
+ def self.put_with_retry(url, body, headers, max_retries: 5)
12
+ attempt = 0
13
+ delay = 1.0
14
+
15
+ begin
16
+ RestClient.put(url, body, headers)
17
+ rescue => e
18
+ status = e.respond_to?(:http_code) ? e.http_code : nil
19
+ retryable = status == 503 ||
20
+ e.is_a?(RestClient::ServerBrokeConnection) ||
21
+ e.is_a?(Errno::ECONNRESET) ||
22
+ (defined?(RestClient::Exceptions::OpenTimeout) && e.is_a?(RestClient::Exceptions::OpenTimeout)) ||
23
+ (defined?(RestClient::Exceptions::ReadTimeout) && e.is_a?(RestClient::Exceptions::ReadTimeout))
24
+
25
+ raise e if !retryable || attempt >= max_retries
26
+
27
+ attempt += 1
28
+ sleep(delay + rand(0.3))
29
+ delay *= 2
30
+ retry
31
+ end
32
+ end
33
+
34
+ def self.upload_artifact(token:, message:, app:, dist_profile_id:, api_endpoint: BASE_URL)
12
35
  file_path = app
13
36
  file_name = File.basename(file_path)
14
37
  file_size = File.size(file_path)
15
38
 
16
- upload_info_url = "#{BASE_URL}/distribution/v1/profiles/#{dist_profile_id}/app-versions"
39
+ upload_info_url = "#{api_endpoint}/distribution/v1/profiles/#{dist_profile_id}/app-versions"
17
40
  headers = {
18
41
  Authorization: "Bearer #{token}",
19
42
  accept: 'application/json'
@@ -38,14 +61,25 @@ module TDUploadService
38
61
  end
39
62
  file_id = upload_info['fileId']
40
63
  upload_url = upload_info['uploadUrl']
64
+ configuration = upload_info['configuration']
65
+ http_method = (configuration && configuration['httpMethod']) ? configuration['httpMethod'].to_s.upcase : 'PUT'
41
66
 
42
- file_content = File.binread(file_path)
43
67
  UI.message("Uploading file to Appcircle...")
44
- response = RestClient.put(
45
- upload_url,
46
- file_content,
47
- { content_type: 'application/octet-stream' }
48
- )
68
+ if http_method == 'POST'
69
+ sign_parameters = configuration['signParameters'] || {}
70
+ payload = {}
71
+ sign_parameters.each { |key, value| payload[key] = value }
72
+ payload['file'] = File.new(file_path, 'rb')
73
+ response = RestClient.post(upload_url, payload)
74
+ else
75
+ file_content = File.binread(file_path)
76
+ response = put_with_retry(
77
+ upload_url,
78
+ file_content,
79
+ { content_type: 'application/octet-stream' }
80
+ )
81
+ end
82
+
49
83
  if response.code.between?(200, 299)
50
84
  UI.success("File upload finished successfully with status code: #{response.code}")
51
85
  else
@@ -53,7 +87,7 @@ module TDUploadService
53
87
  raise "File upload failed."
54
88
  end
55
89
 
56
- commit_url = "#{BASE_URL}/distribution/v1/profiles/#{dist_profile_id}/app-versions"
90
+ commit_url = "#{api_endpoint}/distribution/v1/profiles/#{dist_profile_id}/app-versions"
57
91
  uri = URI(commit_url)
58
92
  uri.query = URI.encode_www_form({ action: 'commitFileUpload' })
59
93
 
@@ -87,8 +121,8 @@ module TDUploadService
87
121
  end
88
122
  end
89
123
 
90
- def self.get_distribution_profiles(auth_token:)
91
- url = "#{BASE_URL}/distribution/v2/profiles"
124
+ def self.get_distribution_profiles(auth_token:, api_endpoint: BASE_URL)
125
+ url = "#{api_endpoint}/distribution/v2/profiles"
92
126
 
93
127
  # Set up the headers with authentication
94
128
  headers = {
@@ -106,8 +140,8 @@ module TDUploadService
106
140
  end
107
141
  end
108
142
 
109
- def self.get_testing_groups(auth_token:)
110
- url = "#{BASE_URL}/distribution/v2/testing-groups"
143
+ def self.get_testing_groups(auth_token:, api_endpoint: BASE_URL)
144
+ url = "#{api_endpoint}/distribution/v2/testing-groups"
111
145
 
112
146
  # Set up the headers with authentication
113
147
  headers = {
@@ -125,8 +159,8 @@ module TDUploadService
125
159
  end
126
160
  end
127
161
 
128
- def self.create_distribution_profile(name:, auth_token:)
129
- url = "#{BASE_URL}/distribution/v2/profiles"
162
+ def self.create_distribution_profile(name:, auth_token:, api_endpoint: BASE_URL)
163
+ url = "#{api_endpoint}/distribution/v2/profiles"
130
164
  headers = {
131
165
  Authorization: "Bearer #{auth_token}",
132
166
  content_type: :json,
@@ -146,8 +180,8 @@ module TDUploadService
146
180
  end
147
181
  end
148
182
 
149
- def self.update_distribution_profile(profile_id:, auth_type:, username:, password:, testing_group_ids:, auth_token:)
150
- url = "#{BASE_URL}/distribution/v2/profiles/#{profile_id}"
183
+ def self.update_distribution_profile(profile_id:, auth_type:, username:, password:, testing_group_ids:, auth_token:, api_endpoint: BASE_URL)
184
+ url = "#{api_endpoint}/distribution/v2/profiles/#{profile_id}"
151
185
  headers = {
152
186
  Authorization: "Bearer #{auth_token}",
153
187
  content_type: :json,
@@ -179,11 +213,11 @@ module TDUploadService
179
213
  end
180
214
  end
181
215
 
182
- def self.get_profile_id(authToken, profileName)
216
+ def self.get_profile_id(authToken, profileName, api_endpoint = BASE_URL)
183
217
  profileId = nil
184
218
 
185
219
  begin
186
- profiles = TDUploadService.get_distribution_profiles(auth_token: authToken)
220
+ profiles = TDUploadService.get_distribution_profiles(auth_token: authToken, api_endpoint: api_endpoint)
187
221
  profiles.each do |profile|
188
222
  if profile["name"] == profileName
189
223
  profileId = profile['id']
@@ -197,12 +231,12 @@ module TDUploadService
197
231
  return profileId
198
232
  end
199
233
 
200
- def self.get_testing_group_ids(authToken, testingGroupNames)
234
+ def self.get_testing_group_ids(authToken, testingGroupNames, api_endpoint = BASE_URL)
201
235
  testingGroupIds = []
202
236
  remainingGroupNames = Set.new(testingGroupNames)
203
237
 
204
238
  begin
205
- groups = TDUploadService.get_testing_groups(auth_token: authToken)
239
+ groups = TDUploadService.get_testing_groups(auth_token: authToken, api_endpoint: api_endpoint)
206
240
 
207
241
  groups.each do |group|
208
242
  if remainingGroupNames.include?(group["name"])
@@ -219,17 +253,18 @@ module TDUploadService
219
253
  return testingGroupIds
220
254
  end
221
255
 
222
- def self.create_profile(authToken, profileName, profileAuthType, profileUsername, profilePassword, profileTestingGroupNames)
256
+ def self.create_profile(authToken, profileName, profileAuthType, profileUsername, profilePassword, profileTestingGroupNames, api_endpoint = BASE_URL)
223
257
  # Get testing group IDs
224
258
  if !profileTestingGroupNames&.empty?
225
- profileTestingGroupIds = TDUploadService.get_testing_group_ids(authToken, profileTestingGroupNames)
259
+ profileTestingGroupIds = TDUploadService.get_testing_group_ids(authToken, profileTestingGroupNames, api_endpoint)
226
260
  end
227
-
261
+
228
262
  # Create
229
263
  begin
230
264
  new_profile = TDUploadService.create_distribution_profile(
231
265
  name: profileName,
232
- auth_token: authToken
266
+ auth_token: authToken,
267
+ api_endpoint: api_endpoint
233
268
  )
234
269
  if new_profile.nil?
235
270
  raise "Error: The new profile could not be created."
@@ -248,7 +283,8 @@ module TDUploadService
248
283
  username: profileUsername,
249
284
  password: profilePassword,
250
285
  testing_group_ids: profileTestingGroupIds,
251
- auth_token: authToken
286
+ auth_token: authToken,
287
+ api_endpoint: api_endpoint
252
288
  )
253
289
  if configured_profile.nil?
254
290
  raise "Error: The new profile could not be configured."
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module AppcircleTestingDistribution
3
- VERSION = "0.4.3.beta.2"
3
+ VERSION = "0.5.0.beta.1"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
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.3.beta.2
4
+ version: 0.5.0.beta.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - appcircleio