fastlane-plugin-appcircle_testing_distribution 0.2.3 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 04a8808916e178499dd62526d02d5408c1161d6a7faf2b3dab95c5e4f71d83bc
4
- data.tar.gz: 5cca9f900d1a7a4ac55be0c3672850c11b66b5ec10d6a954f02a8b740b5838b0
3
+ metadata.gz: e01adda887a2e03213c82a2f1dd4c73e068599c6f2a72107663bc40b0246259d
4
+ data.tar.gz: 551d74f7d9d2ba2f9683fbc20c69c7e06c8897c7a711d98a6d636ce50fab7d82
5
5
  SHA512:
6
- metadata.gz: 6f56fd4fe450019acf7f09b21782fd27ecef0126512ec9ed12685c768218cfc41ac31f48b6b23eeb63442d1af017610be27d3bcacb637279204082accf6bfada
7
- data.tar.gz: 5bd6a6082c8f950098c48cc4a741c4969627bda212e1a3f2246f06301c0c7219aa3be370da94a13d3c8517daf6803637e96329c0b6679d4953ac703e99f624a7
6
+ metadata.gz: d77f4bf600f1281ac845c37c5a37cbe76c2ffffa3630f6d27d12e793912c80c01197ec746c2ef8d15f2c4c7a75e2798faeae10bad0485bb3e1ee5d65fbd5c472
7
+ data.tar.gz: fa09b51a8528c0693bf8a4371b29fd3ba1a87e42185069599744f28d0d06eab8430f8f38af5198fce624b9483ca0e20b6ad51df9f8cdc1565dcec150c7015ff1
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2024 Guven Karanfil <guven.karanfil@smartface.io>
3
+ Copyright (c) 2024 Appcircle, Inc. <info@appcircle.io>
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- ## Appcircle Testing Distribution
1
+ # Appcircle Testing Distribution
2
2
 
3
3
  [![fastlane Plugin Badge](https://rawcdn.githack.com/fastlane/fastlane/master/fastlane/assets/plugin-badge.svg)](https://rubygems.org/gems/fastlane-plugin-appcircle_testing_distribution)
4
4
 
@@ -38,24 +38,11 @@ Testing distribution is the process of distributing test builds to designated te
38
38
 
39
39
  Overall, using testing distribution in mobile DevOps significantly enhances the efficiency, security, and effectiveness of the software development process, leading to better products and faster delivery times.
40
40
 
41
- ## System Requirements
41
+ <!-- ## Testing Distribution
42
42
 
43
- **Compatible Agents:**
43
+ In order to share your builds with testers, you can create testing distribution profiles and assign testing groups to the profiles.
44
44
 
45
- - macOS 14.2, 14.5
46
-
47
- **Supported Version:**
48
-
49
- - Fastlane 2.222.0
50
- - Ruby 3.2.2
51
-
52
- Note: Currently, plugins are only compatible to use with **Appcircle Cloud**. **Self-hosted** support will be available in future releases.
53
-
54
- ## Testing Distribution
55
-
56
- In order to share your builds with testers, you can create distribution profiles and assign testing groups to the distribution profiles.
57
-
58
- ![Distribution Profile](<https://cdn.appcircle.io/docs/assets/image%20(152).png>)
45
+ ![Testing Distribution Profile](<https://cdn.appcircle.io/docs/assets/image%20(152).png>)
59
46
 
60
47
  ## Generating/Managing the Personal API Tokens
61
48
 
@@ -65,11 +52,11 @@ To generate a Personal API Token, follow these steps:
65
52
  2. You'll find the Personal API Token section in the top right corner.
66
53
  3. Press the "Generate Token" button to generate your first token.
67
54
 
68
- ![Token Generation](<https://cdn.appcircle.io/docs/assets/image%20(164).png>)
55
+ ![Token Generation](<https://cdn.appcircle.io/docs/assets/image%20(164).png>) -->
69
56
 
70
57
  ## Getting Started with the Extension: Usage Guide
71
58
 
72
- To share your builds with testers, you can create distribution profiles and assign testing groups to these profiles.
59
+ To share your builds with testers, you can create testing distribution profiles and assign testing groups to these profiles.
73
60
 
74
61
  This project is a [_fastlane_](https://github.com/fastlane/fastlane) plugin. To get started with `fastlane-plugin-appcircle_testing_distribution`, add it to your project by running:
75
62
 
@@ -77,31 +64,48 @@ This project is a [_fastlane_](https://github.com/fastlane/fastlane) plugin. To
77
64
  fastlane add_plugin appcircle_testing_distribution
78
65
  ```
79
66
 
80
- ```yml
67
+ ```ruby
81
68
  appcircle_testing_distribution(
82
- personalAPIToken: "$(AC_PERSONAL_API_TOKEN)",
83
- profileName: "$(AC_PROFILE_NAME)",
84
- createProfileIfNotExists: Boolean,
85
- appPath: "$(AC_APP_PATH)",
86
- message: "$(AC_MESSAGE)",
69
+ personalAPIToken: ENV["AC_PERSONAL_API_TOKEN"],
70
+ subOrganizationName: ENV["AC_SUB_ORGANIZATION_NAME"],
71
+ profileName: ENV["AC_PROFILE_NAME"],
72
+ createProfileIfNotExists: ENV["AC_CREATE_PROFILE_IF_NOT_EXISTS"],
73
+ profileCreationSettings: {
74
+ authType: ENV["AC_PROFILE_AUTH_TYPE"],
75
+ username: ENV["AC_PROFILE_USERNAME"],
76
+ password: ENV["AC_PROFILE_PASSWORD"],
77
+ testingGroupNames: ENV["AC_PROFILE_TESTING_GROUP_NAMES"]
78
+ },
79
+ appPath: ENV["AC_APP_PATH"],
80
+ message: ENV["AC_MESSAGE"]
87
81
  )
88
82
  ```
89
83
 
90
- - `personalAPIToken`: The Appcircle Personal API token is utilized to authenticate and secure access to Appcircle services, ensuring that only authorized users can perform actions within the platform.
84
+ - `personalAPIToken`: The Appcircle Personal API token used to authenticate and authorize access to Appcircle services within this plugin.
85
+ - `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.
91
86
  - `profileName`: Specifies the profile that will be used for uploading the app.
92
- - `createProfileIfNotExists`: Ensures that a user 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.
93
- - `appPath`: Indicates the file path to the application that will be uploaded to Appcircle Testing Distribution Profile.
87
+ - `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.
88
+ - `profileCreationSettings` (optional): If `createProfileIfNotExists` is `true` and a new profile being created, the profile will be configured with these settings.
89
+ - `authType`: Authentication type of the profile. `none`: None, `static`: Static Username and Password, `ldap`: LDAP Login, `sso`: SSO Login.
90
+ - `username`: The username for the profile if authentication type set to `static` (Static Username and Password).
91
+ - `password`: The password for the profile if authentication type set to `static` (Static Username and Password).
92
+ - `testingGroupNames`: Uploaded versions will be automatically shared with these testing groups. Example format: `group1, group2, group3`.
93
+ - `appPath`: Indicates the file path to the application package that will be uploaded to Appcircle Testing Distribution Profile.
94
94
  - `message`: Your message to testers, ensuring they receive important updates and information regarding the application.
95
95
 
96
- ### Leveraging Environment Variables
96
+ ## Further Details
97
97
 
98
- Utilize environment variables seamlessly by substituting the parameters with `$(VARIABLE_NAME)` in your task inputs. The extension automatically retrieves values from the specified environment variables within your pipeline.
98
+ For more information please refer to the documentation.
99
99
 
100
- **Ensure that this action is added after build steps have been completed.**
101
-
102
- **If multiple workflows start simultaneously, the order in which versions are shared in the Testing Distribution is determined by the execution order of the publish step. The version that completes its build and triggers the publish plugin first will be shared first, followed by the others in sequence.**
103
-
104
- Efficiently distribute test binaries or beta versions using Appcircle, featuring seamless IPA and APK distribution capabilities. Streamline your testing process with our versatile tool designed to optimize your distribution workflow. If you need support or more information, please [contact us](https://appcircle.io/contact?utm_source=fastlane&utm_medium=plugin&utm_campaign=testing_distribution)
100
+ - [Setting Up Appcircle Testing Distribution Plugin](https://docs.appcircle.io/marketplace/fastlane/testing-distribution)
101
+ - [Discover Action](https://docs.appcircle.io/marketplace/fastlane/testing-distribution#discover-action)
102
+ - [System Requirements](https://docs.appcircle.io/marketplace/fastlane/testing-distribution#system-requirements)
103
+ - [User Permission Requirements](https://docs.appcircle.io/marketplace/fastlane/testing-distribution#user-permission-requirements)
104
+ - [How to Add the Appcircle Distribute Action to Your Pipeline](https://docs.appcircle.io/marketplace/fastlane/testing-distribution#how-to-add-the-appcircle-distribute-action-to-your-pipeline)
105
+ - [CLI Usage](https://docs.appcircle.io/marketplace/fastlane/testing-distribution#cli-usage)
106
+ - [Distributing to Sub-Organizations](https://docs.appcircle.io/marketplace/fastlane/testing-distribution#distributing-to-sub-organizations)
107
+ - [Leveraging Environment Variables](https://docs.appcircle.io/marketplace/fastlane/testing-distribution#leveraging-environment-variables)
108
+ - [References](https://docs.appcircle.io/marketplace/fastlane/testing-distribution#references)
105
109
 
106
110
  ## Issues and Feedback
107
111
 
@@ -110,9 +114,3 @@ For any other issues and feedback about this plugin, please submit it to this re
110
114
  ## Troubleshooting
111
115
 
112
116
  If you have trouble using plugins, check out the [Plugins Troubleshooting](https://docs.fastlane.tools/plugins/plugins-troubleshooting/) guide.
113
-
114
- ## Reference
115
-
116
- - For details on generating an Appcircle Personal API Token, visit [Generating/Managing Personal API Tokens](https://docs.appcircle.io/appcircle-api/api-authentication#generatingmanaging-the-personal-api-tokens?utm_source=fastlane&utm_medium=plugin&utm_campaign=testing_distribution)
117
-
118
- - To create or learn more about Appcircle testing and distribution profiles, please refer to [Creating or Selecting a Distribution Profile](https://docs.appcircle.io/distribute/create-or-select-a-distribution-profile?utm_source=fastlane&utm_medium=plugin&utm_campaign=testing_distribution)
@@ -10,71 +10,127 @@ require_relative '../helper/TDUploadService'
10
10
  module Fastlane
11
11
  module Actions
12
12
  class AppcircleTestingDistributionAction < Action
13
+ VALID_EXTENSIONS = ['.apk', '.aab', '.ipa', '.zip']
14
+ AUTH_TYPE_MAPPING = {
15
+ 'none' => 1, # None
16
+ 'static' => 3, # Static Username and Password
17
+ 'ldap' => 4, # LDAP Login
18
+ 'sso' => 5 # SSO Login
19
+ }
20
+
13
21
  def self.run(params)
14
22
  personalAPIToken = params[:personalAPIToken]
15
- personalAccessKey = params[:personalAccessKey]
23
+ subOrganizationName = params[:subOrganizationName]
16
24
  profileName = params[:profileName]
25
+ createProfileIfNotExists = params[:createProfileIfNotExists] || false
26
+ #
27
+ profileCreationSettings = params[:profileCreationSettings]
28
+ profileAuthType = profileCreationSettings&.dig(:authType)
29
+ profileUsername = profileCreationSettings&.dig(:username)
30
+ profilePassword = profileCreationSettings&.dig(:password)
31
+ profileTestingGroupNames= profileCreationSettings&.dig(:testingGroupNames)
32
+ #
17
33
  appPath = params[:appPath]
18
34
  message = params[:message]
19
- createProfileIfNotExists = params[:createProfileIfNotExists]
35
+
36
+ profileAuthType = AUTH_TYPE_MAPPING[profileAuthType] # map input to API values
20
37
 
21
- valid_extensions = ['.apk', '.aab', '.ipa', '.zip']
38
+ # Auth
39
+ authToken = self.ac_login(personalAPIToken, subOrganizationName)
22
40
 
23
- file_extension = File.extname(appPath).downcase
24
- unless valid_extensions.include?(file_extension)
25
- UI.user_error!("Invalid file extension: #{file_extension}. For Android, use .apk or .aab. For iOS, use .ipa or .zip(.xcarchive).")
26
- end
41
+ # Get or create profile
42
+ profileId = self.ac_get_or_create_profile(authToken, profileName, createProfileIfNotExists, profileCreationSettings, profileAuthType, profileUsername, profilePassword, profileTestingGroupNames)
43
+
44
+ # Upload package
45
+ self.ac_upload(authToken, appPath, profileId, profileName, message)
46
+ end
47
+
48
+ def self.ac_login(personalAPIToken, subOrganizationName)
49
+ begin
50
+ token = ''
51
+
52
+ user = TDAuthService.get_ac_token(pat: personalAPIToken)
53
+ UI.success("Login is successful.")
54
+ 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
61
+ end
62
+
63
+ return token
27
64
 
28
- if personalAPIToken.nil? && personalAccessKey.nil?
29
- 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")
30
- elsif !personalAPIToken.nil? && !personalAccessKey.nil?
31
- UI.user_error!("Personal API Token and Personal Access Key cannot be used together. Please provide only one authentication method")
32
- elsif profileName.nil?
33
- UI.user_error!("Distribution profile name is required to distribute applications. Please provide a distribution profile name")
34
- elsif appPath.nil?
35
- UI.user_error!("Application file path is required to distribute applications. Please provide a valid application file path")
36
- elsif message.nil?
37
- UI.user_error!("Message field is required. Please provide a valid message")
65
+ rescue => e
66
+ UI.user_error!("Login failed: \"#{e.message}\".")
38
67
  end
68
+ end
69
+
70
+ def self.ac_get_or_create_profile(authToken, profileName, createProfileIfNotExists, profileCreationSettings, profileAuthType, profileUsername, profilePassword, profileTestingGroupNames)
71
+ begin
72
+ profileId = TDUploadService.get_profile_id(authToken, profileName)
39
73
 
40
- authToken = self.ac_login(personalAPIToken, personalAccessKey)
74
+ if profileId
75
+ UI.message("Profile '#{profileName}' found with ID: #{profileId}.")
76
+ UI.important("Warning: Profile '#{profileName}' already exists, so the provided profile creation settings will be ignored. To update the profile settings, please use the Appcircle web interface.") if profileCreationSettings
77
+
78
+ elsif profileId.nil? && !createProfileIfNotExists
79
+ 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.")
80
+ elsif profileId.nil? && createProfileIfNotExists
81
+ UI.message("Profile '#{profileName}' not found. Creating the new profile...")
82
+ profileId = TDUploadService.create_profile(authToken, profileName, profileAuthType, profileUsername, profilePassword, profileTestingGroupNames)
83
+ end
41
84
 
42
- profileId = TDUploadService.get_profile_id(authToken, profileName, createProfileIfNotExists)
43
- self.ac_upload(authToken, appPath, profileId, message)
85
+ return profileId
86
+
87
+ rescue => e
88
+ UI.user_error!("Couldn't get the profile: \"#{e.message}\".")
89
+ end
44
90
  end
45
91
 
46
- def self.ac_login(personalAPIToken, personalAccessKey)
47
- if personalAccessKey
48
- user = TDAuthService.get_ac_token_with_personal_access_key(personal_access_key: personalAccessKey)
49
- else
50
- user = TDAuthService.get_ac_token(pat: personalAPIToken)
92
+ def self.ac_upload(token, appPath, profileID, profileName, message)
93
+ begin
94
+ UI.message("Upload started.")
95
+ response = TDUploadService.upload_artifact(token: token, message: message, app: appPath, dist_profile_id: profileID)
96
+ result = self.checkTaskStatus(token, response['taskId'])
97
+
98
+ if result
99
+ UI.success("#{appPath} uploaded to profile '#{profileName}' successfully 🎉")
100
+ end
101
+ rescue => e
102
+ status_code = e.respond_to?(:response) && e.response ? e.response.code : 'unknown'
103
+ UI.user_error!("Upload failed with status code '#{status_code}', with message \"#{e.message}\".")
51
104
  end
52
- UI.success("Login is successful.")
53
- return user.accessToken
54
- rescue StandardError => e
55
- UI.user_error!("Login failed: #{e.message}")
56
105
  end
57
106
 
58
107
  def self.checkTaskStatus(authToken, taskId)
59
108
  uri = URI.parse("https://api.appcircle.io/task/v1/tasks/#{taskId}")
60
- timeout = 1
61
-
62
- response = self.send_request(uri, authToken)
63
- if response.kind_of?(Net::HTTPSuccess)
64
- stateValue = JSON.parse(response.body)["stateValue"]
65
-
66
- if stateValue == 1
67
- sleep(1)
68
- return checkTaskStatus(authToken, taskId)
69
- end
70
- if stateValue == 3
71
- return true
109
+
110
+ check_interval = 1
111
+ # timeout = 2 * 60 * 60 # 2 hours in seconds
112
+ # start_time = Time.now
113
+
114
+ loop do
115
+ response = self.send_request(uri, authToken)
116
+ if response.is_a?(Net::HTTPSuccess)
117
+ stateValue = JSON.parse(response.body)["stateValue"]
118
+
119
+ if stateValue == 1
120
+ sleep(check_interval)
121
+ elsif stateValue == 3
122
+ return true
123
+ else
124
+ UI.error("Task Id #{taskId} failed with state value #{stateValue}.")
125
+ UI.user_error!("Upload could not be completed successfully.")
126
+ end
72
127
  else
73
- UI.error("Task Id #{taskId} failed with state value #{stateValue}")
74
- raise "Upload could not completed successfully"
128
+ UI.user_error!("Upload failed with response code #{response.code} and message '#{response.message}'.")
75
129
  end
76
- else
77
- raise "Upload failed with response code #{response.code} and message '#{response.message}'"
130
+
131
+ # if Time.now - start_time > timeout
132
+ # UI.user_error!("Task Id #{taskId} timed out after 2 hours.")
133
+ # end
78
134
  end
79
135
  end
80
136
 
@@ -86,18 +142,6 @@ module Fastlane
86
142
  http.request(request)
87
143
  end
88
144
 
89
- def self.ac_upload(token, appPath, profileID, message)
90
- response = TDUploadService.upload_artifact(token: token, message: message, app: appPath, dist_profile_id: profileID)
91
- result = self.checkTaskStatus(token, response['taskId'])
92
-
93
- if result
94
- UI.success("#{appPath} Uploaded to profile id #{profileID} successfully 🎉")
95
- end
96
- rescue StandardError => e
97
- status_code = e.respond_to?(:response) && e.response ? e.response.code : 'unknown'
98
- UI.user_error!("Upload failed with status code #{status_code}, with message '#{e.message}'")
99
- end
100
-
101
145
  def self.description
102
146
  "Efficiently distribute application builds to users or testing groups using Appcircle's robust platform."
103
147
  end
@@ -112,46 +156,81 @@ module Fastlane
112
156
 
113
157
  def self.details
114
158
  # Optional:
115
- "Appcircle simplifies the distribution of builds to test teams with an extensive platform for managing and tracking applications, versions, testers, and teams. Appcircle integrates with enterprise authentication mechanisms such as LDAP and SSO, ensuring secure distribution of testing packages. Learn more about Appcircle testing distribution"
159
+ "Appcircle simplifies the distribution of builds to test teams with an extensive platform for managing and tracking applications, versions, testers, and teams. Appcircle integrates with enterprise authentication mechanisms such as LDAP and SSO, ensuring secure distribution of testing packages. Learn more about Appcircle testing distribution."
116
160
  end
117
161
 
118
162
  def self.available_options
119
163
  [
120
164
  FastlaneCore::ConfigItem.new(key: :personalAPIToken,
121
- env_name: "AC_PERSONAL_API_TOKEN",
122
- description: "Provide Personal API Token to authenticate connections to Appcircle services (alternative to personalAccessKey)",
123
- optional: true,
124
- type: String),
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),
125
171
 
126
- FastlaneCore::ConfigItem.new(key: :personalAccessKey,
127
- env_name: "AC_PERSONAL_ACCESS_KEY",
128
- description: "Provide Personal Access Key to authenticate connections to Appcircle services (alternative to personalAPIToken)",
172
+ FastlaneCore::ConfigItem.new(key: :subOrganizationName,
173
+ description: "Optional: Sub-organization name for app distribution. Profiles will be created under root organization if not provided",
129
174
  optional: true,
130
175
  type: String),
131
176
 
132
177
  FastlaneCore::ConfigItem.new(key: :profileName,
133
- env_name: "AC_PROFILE_NAME",
134
- description: "Enter the profile name of the Appcircle distribution profile. This name uniquely identifies the profile under which your applications will be distributed",
178
+ description: "Enter the profile name of the Appcircle testing distribution profile. This name uniquely identifies the profile under which your applications will be distributed",
135
179
  optional: false,
136
- type: String),
180
+ type: String,
181
+ verify_block: proc do |value|
182
+ UI.user_error!("Profile name cannot be empty. Please provide a testing distribution profile name.") unless value && !value.empty?
183
+ end),
137
184
 
138
185
  FastlaneCore::ConfigItem.new(key: :createProfileIfNotExists,
139
- env_name: "AC_CREATE_PROFILE_IF_NOT_EXISTS",
140
- description: "If the profile does not exist, create a new profile with the given name",
186
+ description: "Optional: If the profile does not exist, create a new profile with the given name",
141
187
  optional: true,
142
188
  type: Boolean),
143
189
 
190
+ FastlaneCore::ConfigItem.new(key: :profileCreationSettings,
191
+ description: "Optional: Profile creation settings for the testing distribution profile",
192
+ optional: true,
193
+ type: Hash,
194
+ verify_block: proc do |value|
195
+ # Parse and Validate
196
+ if value[:authType] && !value[:authType].empty?
197
+ UI.user_error!("Invalid authType: '#{value[:authType]}'. Options: 'none' (None), 'static' (Static Username and Password), 'ldap' (LDAP Login), 'sso' (SSO Login).") unless AUTH_TYPE_MAPPING.key?(value[:authType])
198
+
199
+ if value[:authType] == 'static'
200
+ UI.user_error!("username must be a String and at least 6 characters long.") unless value[:username].kind_of?(String) && value[:username].length >= 6
201
+ UI.user_error!("password must be a String and at least 6 characters long.") unless value[:password].kind_of?(String) && value[:password].length >= 6
202
+ else
203
+ value[:username] = nil
204
+ value[:password] = nil
205
+ end
206
+ end
207
+
208
+ if value[:testingGroupNames] && !value[:testingGroupNames].empty?
209
+ value[:testingGroupNames] = value[:testingGroupNames].to_s.split(",").map(&:strip)
210
+ UI.user_error!("testingGroupNames must be a string array. Ex: 'group1, group2, group3'.") unless value[:testingGroupNames].kind_of?(Array)
211
+ end
212
+ end),
213
+
144
214
  FastlaneCore::ConfigItem.new(key: :appPath,
145
- env_name: "AC_APP_PATH",
146
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",
147
216
  optional: false,
148
- type: String),
217
+ type: String,
218
+ verify_block: proc do |value|
219
+ UI.user_error!("Application file path cannot be empty. Please provide a valid application file path.") unless value && !value.empty?
220
+
221
+ file_extension = File.extname(value).downcase
222
+ 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).")
224
+ end
225
+ end),
149
226
 
150
227
  FastlaneCore::ConfigItem.new(key: :message,
151
- env_name: "AC_MESSAGE",
152
- description: "Optional message to include with the distribution to provide additional information to testers or users receiving the build",
228
+ description: "Message to include with the distribution to provide additional information to testers or users receiving the build",
153
229
  optional: false,
154
- type: String)
230
+ type: String,
231
+ verify_block: proc do |value|
232
+ UI.user_error!("Message field cannot be empty. Please provide a message.") unless value && !value.empty?
233
+ end)
155
234
  ]
156
235
  end
157
236
 
@@ -3,6 +3,7 @@ require 'uri'
3
3
  require 'cgi'
4
4
  require 'json'
5
5
 
6
+
6
7
  class UserResponse
7
8
  attr_accessor :accessToken
8
9
 
@@ -12,7 +13,7 @@ class UserResponse
12
13
  end
13
14
 
14
15
  module TDAuthService
15
- def self.get_ac_token(pat:)
16
+ def self.get_ac_token(pat:, sub_organization_id: nil)
16
17
  endpoint_url = 'https://auth.appcircle.io/auth/v2/token'
17
18
  uri = URI(endpoint_url)
18
19
 
@@ -20,9 +21,10 @@ module TDAuthService
20
21
  request = Net::HTTP::Post.new(uri)
21
22
  request.content_type = 'application/x-www-form-urlencoded'
22
23
  request['Accept'] = 'application/json'
23
-
24
+
24
25
  # Encode parameters
25
26
  params = { pat: pat }
27
+ params[:subOrganizationId] = sub_organization_id if sub_organization_id
26
28
  request.body = URI.encode_www_form(params)
27
29
 
28
30
  # Make the HTTP request
@@ -31,7 +33,7 @@ module TDAuthService
31
33
  end
32
34
 
33
35
  # Check response
34
- if response.kind_of?(Net::HTTPSuccess)
36
+ if response.is_a?(Net::HTTPSuccess)
35
37
  response_data = JSON.parse(response.body)
36
38
 
37
39
  user = UserResponse.new(
@@ -40,39 +42,35 @@ module TDAuthService
40
42
 
41
43
  return user
42
44
  else
43
- raise "HTTP Request failed (#{response.code} #{response.message})"
45
+ raise "Error: (#{response.code} #{response.message})."
44
46
  end
45
47
  end
46
48
 
47
- def self.get_ac_token_with_personal_access_key(personal_access_key:)
48
- endpoint_url = 'https://auth.appcircle.io/auth/v1/token'
49
+ def self.get_organization_id(access_token:, name:)
50
+ endpoint_url = 'https://api.appcircle.io/identity/v1/organizations'
49
51
  uri = URI(endpoint_url)
50
-
52
+
51
53
  # Create HTTP request
52
- request = Net::HTTP::Post.new(uri)
53
- request.content_type = 'application/x-www-form-urlencoded'
54
+ request = Net::HTTP::Get.new(uri)
55
+ request['Authorization'] = "Bearer #{access_token}"
54
56
  request['Accept'] = 'application/json'
55
-
56
- # Encode parameters
57
- params = { 'personal-access-key' => personal_access_key }
58
- request.body = URI.encode_www_form(params)
59
-
57
+
60
58
  # Make the HTTP request
61
59
  response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
62
60
  http.request(request)
63
61
  end
64
-
62
+
65
63
  # Check response
66
- if response.kind_of?(Net::HTTPSuccess)
64
+ if response.is_a?(Net::HTTPSuccess)
67
65
  response_data = JSON.parse(response.body)
66
+ organizations = response_data['data']
67
+ organization = organizations.find { |org| org['name'] == name }
68
+
69
+ raise "Organization with name '#{name}' not found" unless organization
70
+ return organization['id']
68
71
 
69
- user = UserResponse.new(
70
- accessToken: response_data['access_token']
71
- )
72
-
73
- return user
74
72
  else
75
- raise "HTTP Request failed (#{response.code} #{response.message})"
73
+ raise "Error: (#{response.code} #{response.message})"
76
74
  end
77
75
  end
78
- end
76
+ end
@@ -9,21 +9,17 @@ module TDUploadService
9
9
  def self.upload_artifact(token:, message:, app:, dist_profile_id:)
10
10
  url = "https://api.appcircle.io/distribution/v2/profiles/#{dist_profile_id}/app-versions"
11
11
  headers = {
12
- Authorization: "Bearer #{token}",
13
- content_type: :multipart # multipart/form-data
12
+ Authorization: "Bearer #{token}"
14
13
  }
15
14
  payload = {
16
15
  Message: message,
17
- File: File.new(app, 'rb')
16
+ File: File.new(app, 'rb'),
17
+ multipart: true # Force multipart encoding for RestClient
18
18
  }
19
-
19
+
20
20
  begin
21
21
  response = RestClient.post(url, payload, headers)
22
- begin
23
- JSON.parse(response.body)
24
- rescue StandardError
25
- response.body
26
- end
22
+ JSON.parse(response.body) rescue response.body
27
23
  rescue RestClient::ExceptionWithResponse => e
28
24
  raise e
29
25
  rescue StandardError => e
@@ -33,13 +29,32 @@ module TDUploadService
33
29
 
34
30
  def self.get_distribution_profiles(auth_token:)
35
31
  url = "#{BASE_URL}/distribution/v2/profiles"
36
-
32
+
37
33
  # Set up the headers with authentication
38
34
  headers = {
39
35
  Authorization: "Bearer #{auth_token}",
40
36
  accept: 'application/json'
41
37
  }
38
+
39
+ begin
40
+ response = RestClient.get(url, headers)
41
+ JSON.parse(response.body)
42
+ rescue RestClient::ExceptionWithResponse => e
43
+ raise e
44
+ rescue StandardError => e
45
+ raise e
46
+ end
47
+ end
42
48
 
49
+ def self.get_testing_groups(auth_token:)
50
+ url = "#{BASE_URL}/distribution/v2/testing-groups"
51
+
52
+ # Set up the headers with authentication
53
+ headers = {
54
+ Authorization: "Bearer #{auth_token}",
55
+ accept: 'application/json'
56
+ }
57
+
43
58
  begin
44
59
  response = RestClient.get(url, headers)
45
60
  JSON.parse(response.body)
@@ -60,7 +75,7 @@ module TDUploadService
60
75
  payload = {
61
76
  name: name
62
77
  }.to_json
63
-
78
+
64
79
  begin
65
80
  response = RestClient.post(url, payload, headers)
66
81
  JSON.parse(response.body)
@@ -71,7 +86,40 @@ module TDUploadService
71
86
  end
72
87
  end
73
88
 
74
- def self.get_profile_id(authToken, profileName, createProfileIfNotExists)
89
+ def self.update_distribution_profile(profile_id:, auth_type:, username:, password:, testing_group_ids:, auth_token:)
90
+ url = "#{BASE_URL}/distribution/v2/profiles/#{profile_id}"
91
+ headers = {
92
+ Authorization: "Bearer #{auth_token}",
93
+ content_type: :json,
94
+ accept: 'application/json-patch+json'
95
+ }
96
+
97
+ ### Construct the payload
98
+ payload = {}
99
+
100
+ settings_payload = {
101
+ authenticationType: auth_type,
102
+ username: username,
103
+ password: password
104
+ }.compact
105
+
106
+ payload[:settings] = settings_payload unless settings_payload.empty?
107
+ payload[:testingGroupIds] = testing_group_ids unless testing_group_ids&.empty?
108
+
109
+ payload = payload.compact.to_json
110
+ ###
111
+
112
+ begin
113
+ response = RestClient.patch(url, payload, headers)
114
+ JSON.parse(response.body)
115
+ rescue RestClient::ExceptionWithResponse => e
116
+ raise e
117
+ rescue StandardError => e
118
+ raise e
119
+ end
120
+ end
121
+
122
+ def self.get_profile_id(authToken, profileName)
75
123
  profileId = nil
76
124
 
77
125
  begin
@@ -79,30 +127,78 @@ module TDUploadService
79
127
  profiles.each do |profile|
80
128
  if profile["name"] == profileName
81
129
  profileId = profile['id']
130
+ break
82
131
  end
83
132
  end
84
- rescue StandardError => e
85
- raise "Something went wrong while fetching profiles: #{e.message}"
133
+ rescue => e
134
+ raise "Something went wrong while fetching profiles: #{e.message}."
86
135
  end
87
136
 
88
- if profileId.nil? && !createProfileIfNotExists
89
- raise "Error: The test profile '#{profileName}' could not be found. The option 'createProfileIfNotExists' is set to false, so no new profile was created. To automatically create a new profile if it doesn't exist, set 'createProfileIfNotExists' to true."
90
- end
137
+ return profileId
138
+ end
139
+
140
+ def self.get_testing_group_ids(authToken, testingGroupNames)
141
+ testingGroupIds = []
142
+ remainingGroupNames = Set.new(testingGroupNames)
91
143
 
92
- if profileId.nil? && createProfileIfNotExists
93
- begin
94
- puts("The test profile '#{profileName}' could not be found. A new profile is being created...")
95
- new_profile = TDUploadService.create_distribution_profile(name: profileName, auth_token: authToken)
96
- if new_profile.nil?
97
- raise "Error: The new profile could not be created."
144
+ begin
145
+ groups = TDUploadService.get_testing_groups(auth_token: authToken)
146
+
147
+ groups.each do |group|
148
+ if remainingGroupNames.include?(group["name"])
149
+ testingGroupIds.push(group['id'])
150
+ remainingGroupNames.delete(group["name"])
98
151
  end
152
+ end
153
+ rescue => e
154
+ raise "Something went wrong while fetching testing groups: #{e.message}."
155
+ end
156
+
157
+ raise "Following testing groups couldn't be found: '#{remainingGroupNames.to_a.join(', ')}'. Aborting profile creation..." unless remainingGroupNames.empty?
99
158
 
100
- profileId = new_profile['id']
101
- rescue StandardError => e
102
- raise "Something went wrong while creating a new profile: #{e.message}"
159
+ return testingGroupIds
160
+ end
161
+
162
+ def self.create_profile(authToken, profileName, profileAuthType, profileUsername, profilePassword, profileTestingGroupNames)
163
+ # Get testing group IDs
164
+ if !profileTestingGroupNames&.empty?
165
+ profileTestingGroupIds = TDUploadService.get_testing_group_ids(authToken, profileTestingGroupNames)
166
+ end
167
+
168
+ # Create
169
+ begin
170
+ new_profile = TDUploadService.create_distribution_profile(
171
+ name: profileName,
172
+ auth_token: authToken
173
+ )
174
+ if new_profile.nil?
175
+ raise "Error: The new profile could not be created."
176
+ end
177
+ profileId = new_profile['id']
178
+ rescue => e
179
+ raise "Something went wrong while creating a new profile: #{e.message}."
180
+ end
181
+
182
+ # Configure
183
+ begin
184
+ Fastlane::UI.message("Configuring the profile...")
185
+ configured_profile = TDUploadService.update_distribution_profile(
186
+ profile_id: profileId,
187
+ auth_type: profileAuthType,
188
+ username: profileUsername,
189
+ password: profilePassword,
190
+ testing_group_ids: profileTestingGroupIds,
191
+ auth_token: authToken
192
+ )
193
+ if configured_profile.nil?
194
+ raise "Error: The new profile could not be configured."
103
195
  end
196
+ profileId = configured_profile['id'] # Should be the same as before
197
+ rescue => e
198
+ raise "Something went wrong while configuring the new profile: #{e.message}."
104
199
  end
105
200
 
106
201
  return profileId
107
202
  end
203
+
108
204
  end
@@ -8,9 +8,6 @@ module Fastlane
8
8
  # class methods that you define here become available in your action
9
9
  # as `Helper::AppcircleTestingDistributionHelper.your_method`
10
10
  #
11
- def self.show_message
12
- UI.message("Hello from the appcircle_testing_distribution plugin helper!")
13
- end
14
11
  end
15
12
  end
16
13
  end
@@ -1,5 +1,5 @@
1
1
  module Fastlane
2
2
  module AppcircleTestingDistribution
3
- VERSION = "0.2.3"
3
+ VERSION = "0.4.0"
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.2.3
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - appcircleio
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-11-24 00:00:00.000000000 Z
11
+ date: 2024-12-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rest-client
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '2.0'
19
+ version: '0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '2.0'
26
+ version: '0'
27
27
  description:
28
28
  email: cloud@appcircle.io
29
29
  executables: []
@@ -32,8 +32,6 @@ extra_rdoc_files: []
32
32
  files:
33
33
  - LICENSE
34
34
  - README.md
35
- - images/PAT.png
36
- - images/distribution-start.png
37
35
  - images/extension-icon.png
38
36
  - lib/fastlane/plugin/appcircle_testing_distribution.rb
39
37
  - lib/fastlane/plugin/appcircle_testing_distribution/actions/appcircle_testing_distribution_action.rb
@@ -45,7 +43,7 @@ homepage: https://github.com/appcircleio/fastlane_plugin_appcircle_testing_distr
45
43
  licenses:
46
44
  - MIT
47
45
  metadata:
48
- rubygems_mfa_required: 'true'
46
+ rubygems_mfa_required: 'false'
49
47
  post_install_message:
50
48
  rdoc_options: []
51
49
  require_paths:
@@ -61,7 +59,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
61
59
  - !ruby/object:Gem::Version
62
60
  version: '0'
63
61
  requirements: []
64
- rubygems_version: 3.4.10
62
+ rubygems_version: 3.4.19
65
63
  signing_key:
66
64
  specification_version: 4
67
65
  summary: Efficiently distribute application builds to users or testing groups using
data/images/PAT.png DELETED
Binary file
Binary file