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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e01adda887a2e03213c82a2f1dd4c73e068599c6f2a72107663bc40b0246259d
4
- data.tar.gz: 551d74f7d9d2ba2f9683fbc20c69c7e06c8897c7a711d98a6d636ce50fab7d82
3
+ metadata.gz: 213b75b8d9c150a9206153fa4bb835277992aac5ba70544ac201337a53eae712
4
+ data.tar.gz: 6dc6d2d960d77b3b7db6976b58aeefc17e0cfb3164bc52d137779a2f7765ae36
5
5
  SHA512:
6
- metadata.gz: d77f4bf600f1281ac845c37c5a37cbe76c2ffffa3630f6d27d12e793912c80c01197ec746c2ef8d15f2c4c7a75e2798faeae10bad0485bb3e1ee5d65fbd5c472
7
- data.tar.gz: fa09b51a8528c0693bf8a4371b29fd3ba1a87e42185069599744f28d0d06eab8430f8f38af5198fce624b9483ca0e20b6ad51df9f8cdc1565dcec150c7015ff1
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 .xcarchive .IPA, APK, AAB, Zip, files directly, avoiding the need to use App Store TestFlight or Google Play Internal Testing.
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', '.zip']
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, subOrganizationName)
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(personalAPIToken, subOrganizationName)
58
+ def self.ac_login(personal_api_token:, personal_access_key:, sub_organization_name:)
49
59
  begin
50
60
  token = ''
51
61
 
52
- user = TDAuthService.get_ac_token(pat: personalAPIToken)
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 subOrganizationName
57
- organization_id = TDAuthService.get_organization_id(access_token: token, name: subOrganizationName)
58
- user = TDAuthService.get_ac_token(pat: personalAPIToken, sub_organization_id: organization_id)
59
- UI.message("Switched to sub-organization: #{subOrganizationName}")
60
- token = user.accessToken
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
- description: "Provide Personal API Token to authenticate connections to Appcircle services",
166
- optional: false,
167
- type: String,
168
- verify_block: proc do |value|
169
- UI.user_error!("Personal API Token cannot be empty. Please provide a valid access token.") unless value && !value.empty?
170
- end),
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
- description: "Specify the path to your application file. For iOS, this can be a .ipa or .xcarchive file path. For Android, specify the .apk or .appbundle file path",
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 or .zip(.xcarchive).")
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
- url = "https://api.appcircle.io/distribution/v2/profiles/#{dist_profile_id}/app-versions"
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
- response = RestClient.post(url, payload, headers)
22
- JSON.parse(response.body) rescue response.body
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
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module AppcircleTestingDistribution
3
- VERSION = "0.4.0"
3
+ VERSION = "0.4.2"
4
4
  end
5
5
  end
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.0
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: 2024-12-18 00:00:00.000000000 Z
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.19
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