fastlane-plugin-appcircle_testing_distribution 0.2.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +39 -41
- data/lib/fastlane/plugin/appcircle_testing_distribution/actions/appcircle_testing_distribution_action.rb +153 -66
- data/lib/fastlane/plugin/appcircle_testing_distribution/helper/TDAuthService.rb +31 -4
- data/lib/fastlane/plugin/appcircle_testing_distribution/helper/TDUploadService.rb +118 -21
- data/lib/fastlane/plugin/appcircle_testing_distribution/helper/appcircle_testing_distribution_helper.rb +0 -3
- data/lib/fastlane/plugin/appcircle_testing_distribution/version.rb +1 -1
- metadata +2 -4
- data/images/PAT.png +0 -0
- data/images/distribution-start.png +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e01adda887a2e03213c82a2f1dd4c73e068599c6f2a72107663bc40b0246259d
|
4
|
+
data.tar.gz: 551d74f7d9d2ba2f9683fbc20c69c7e06c8897c7a711d98a6d636ce50fab7d82
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
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
|
-
##
|
41
|
+
<!-- ## Testing Distribution
|
42
42
|
|
43
|
-
|
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
|
-
|
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
|
-
```
|
67
|
+
```ruby
|
81
68
|
appcircle_testing_distribution(
|
82
|
-
personalAPIToken: "
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
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
|
93
|
-
- `
|
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
|
-
|
96
|
+
## Further Details
|
97
97
|
|
98
|
-
|
98
|
+
For more information please refer to the documentation.
|
99
99
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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,67 +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]
|
23
|
+
subOrganizationName = params[:subOrganizationName]
|
15
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
|
+
#
|
16
33
|
appPath = params[:appPath]
|
17
34
|
message = params[:message]
|
18
|
-
|
35
|
+
|
36
|
+
profileAuthType = AUTH_TYPE_MAPPING[profileAuthType] # map input to API values
|
19
37
|
|
20
|
-
|
38
|
+
# Auth
|
39
|
+
authToken = self.ac_login(personalAPIToken, subOrganizationName)
|
21
40
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
26
64
|
|
27
|
-
|
28
|
-
|
29
|
-
elsif profileName.nil?
|
30
|
-
raise UI.error("Distribution profile name is required to distribute applications. Please provide a distribution profile name")
|
31
|
-
elsif appPath.nil?
|
32
|
-
raise UI.error("Application file path is required to distribute applications. Please provide a valid application file path")
|
33
|
-
elsif message.nil?
|
34
|
-
raise UI.error("Message field is required. Please provide a valid message")
|
65
|
+
rescue => e
|
66
|
+
UI.user_error!("Login failed: \"#{e.message}\".")
|
35
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)
|
36
73
|
|
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
|
37
77
|
|
38
|
-
|
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
|
39
84
|
|
40
|
-
|
41
|
-
|
85
|
+
return profileId
|
86
|
+
|
87
|
+
rescue => e
|
88
|
+
UI.user_error!("Couldn't get the profile: \"#{e.message}\".")
|
89
|
+
end
|
42
90
|
end
|
43
91
|
|
44
|
-
def self.
|
92
|
+
def self.ac_upload(token, appPath, profileID, profileName, message)
|
45
93
|
begin
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
49
101
|
rescue => e
|
50
|
-
|
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
105
|
end
|
53
|
-
|
106
|
+
|
54
107
|
def self.checkTaskStatus(authToken, taskId)
|
55
108
|
uri = URI.parse("https://api.appcircle.io/task/v1/tasks/#{taskId}")
|
56
|
-
timeout = 1
|
57
109
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
68
127
|
else
|
69
|
-
UI.
|
70
|
-
raise "Upload could not completed successfully"
|
128
|
+
UI.user_error!("Upload failed with response code #{response.code} and message '#{response.message}'.")
|
71
129
|
end
|
72
|
-
|
73
|
-
|
130
|
+
|
131
|
+
# if Time.now - start_time > timeout
|
132
|
+
# UI.user_error!("Task Id #{taskId} timed out after 2 hours.")
|
133
|
+
# end
|
74
134
|
end
|
75
135
|
end
|
76
136
|
|
@@ -82,20 +142,6 @@ module Fastlane
|
|
82
142
|
http.request(request)
|
83
143
|
end
|
84
144
|
|
85
|
-
def self.ac_upload(token, appPath, profileID, message)
|
86
|
-
begin
|
87
|
-
response = TDUploadService.upload_artifact(token: token, message: message, app: appPath, dist_profile_id: profileID)
|
88
|
-
result = self.checkTaskStatus(token, response['taskId'])
|
89
|
-
|
90
|
-
if result
|
91
|
-
UI.success("#{appPath} Uploaded to profile id #{profileID} successfully 🎉")
|
92
|
-
end
|
93
|
-
rescue => e
|
94
|
-
status_code = e.respond_to?(:response) && e.response ? e.response.code : 'unknown'
|
95
|
-
UI.error("Upload failed with status code #{status_code}, with message '#{e.message}'")
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
145
|
def self.description
|
100
146
|
"Efficiently distribute application builds to users or testing groups using Appcircle's robust platform."
|
101
147
|
end
|
@@ -110,40 +156,81 @@ module Fastlane
|
|
110
156
|
|
111
157
|
def self.details
|
112
158
|
# Optional:
|
113
|
-
"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."
|
114
160
|
end
|
115
161
|
|
116
162
|
def self.available_options
|
117
163
|
[
|
118
164
|
FastlaneCore::ConfigItem.new(key: :personalAPIToken,
|
119
|
-
env_name: "AC_PERSONAL_API_TOKEN",
|
120
165
|
description: "Provide Personal API Token to authenticate connections to Appcircle services",
|
121
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),
|
171
|
+
|
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",
|
174
|
+
optional: true,
|
122
175
|
type: String),
|
123
|
-
|
176
|
+
|
124
177
|
FastlaneCore::ConfigItem.new(key: :profileName,
|
125
|
-
|
126
|
-
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",
|
127
179
|
optional: false,
|
128
|
-
type: String
|
129
|
-
|
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),
|
184
|
+
|
130
185
|
FastlaneCore::ConfigItem.new(key: :createProfileIfNotExists,
|
131
|
-
|
132
|
-
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",
|
133
187
|
optional: true,
|
134
188
|
type: Boolean),
|
135
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
|
+
|
136
214
|
FastlaneCore::ConfigItem.new(key: :appPath,
|
137
|
-
env_name: "AC_APP_PATH",
|
138
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",
|
139
216
|
optional: false,
|
140
|
-
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),
|
141
226
|
|
142
227
|
FastlaneCore::ConfigItem.new(key: :message,
|
143
|
-
|
144
|
-
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",
|
145
229
|
optional: false,
|
146
|
-
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)
|
147
234
|
]
|
148
235
|
end
|
149
236
|
|
@@ -13,7 +13,7 @@ class UserResponse
|
|
13
13
|
end
|
14
14
|
|
15
15
|
module TDAuthService
|
16
|
-
def self.get_ac_token(pat:)
|
16
|
+
def self.get_ac_token(pat:, sub_organization_id: nil)
|
17
17
|
endpoint_url = 'https://auth.appcircle.io/auth/v2/token'
|
18
18
|
uri = URI(endpoint_url)
|
19
19
|
|
@@ -24,6 +24,7 @@ module TDAuthService
|
|
24
24
|
|
25
25
|
# Encode parameters
|
26
26
|
params = { pat: pat }
|
27
|
+
params[:subOrganizationId] = sub_organization_id if sub_organization_id
|
27
28
|
request.body = URI.encode_www_form(params)
|
28
29
|
|
29
30
|
# Make the HTTP request
|
@@ -31,8 +32,6 @@ module TDAuthService
|
|
31
32
|
http.request(request)
|
32
33
|
end
|
33
34
|
|
34
|
-
|
35
|
-
|
36
35
|
# Check response
|
37
36
|
if response.is_a?(Net::HTTPSuccess)
|
38
37
|
response_data = JSON.parse(response.body)
|
@@ -43,7 +42,35 @@ module TDAuthService
|
|
43
42
|
|
44
43
|
return user
|
45
44
|
else
|
46
|
-
raise "
|
45
|
+
raise "Error: (#{response.code} #{response.message})."
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.get_organization_id(access_token:, name:)
|
50
|
+
endpoint_url = 'https://api.appcircle.io/identity/v1/organizations'
|
51
|
+
uri = URI(endpoint_url)
|
52
|
+
|
53
|
+
# Create HTTP request
|
54
|
+
request = Net::HTTP::Get.new(uri)
|
55
|
+
request['Authorization'] = "Bearer #{access_token}"
|
56
|
+
request['Accept'] = 'application/json'
|
57
|
+
|
58
|
+
# Make the HTTP request
|
59
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
60
|
+
http.request(request)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Check response
|
64
|
+
if response.is_a?(Net::HTTPSuccess)
|
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']
|
71
|
+
|
72
|
+
else
|
73
|
+
raise "Error: (#{response.code} #{response.message})"
|
47
74
|
end
|
48
75
|
end
|
49
76
|
end
|
@@ -9,13 +9,12 @@ 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
|
-
Message: message,
|
14
|
-
content_type: :multipart
|
12
|
+
Authorization: "Bearer #{token}"
|
15
13
|
}
|
16
14
|
payload = {
|
17
15
|
Message: message,
|
18
|
-
File: File.new(app, 'rb')
|
16
|
+
File: File.new(app, 'rb'),
|
17
|
+
multipart: true # Force multipart encoding for RestClient
|
19
18
|
}
|
20
19
|
|
21
20
|
begin
|
@@ -39,9 +38,26 @@ module TDUploadService
|
|
39
38
|
|
40
39
|
begin
|
41
40
|
response = RestClient.get(url, headers)
|
42
|
-
|
41
|
+
JSON.parse(response.body)
|
42
|
+
rescue RestClient::ExceptionWithResponse => e
|
43
|
+
raise e
|
44
|
+
rescue StandardError => e
|
45
|
+
raise e
|
46
|
+
end
|
47
|
+
end
|
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
|
+
}
|
43
57
|
|
44
|
-
|
58
|
+
begin
|
59
|
+
response = RestClient.get(url, headers)
|
60
|
+
JSON.parse(response.body)
|
45
61
|
rescue RestClient::ExceptionWithResponse => e
|
46
62
|
raise e
|
47
63
|
rescue StandardError => e
|
@@ -70,7 +86,40 @@ module TDUploadService
|
|
70
86
|
end
|
71
87
|
end
|
72
88
|
|
73
|
-
def self.
|
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)
|
74
123
|
profileId = nil
|
75
124
|
|
76
125
|
begin
|
@@ -78,27 +127,75 @@ module TDUploadService
|
|
78
127
|
profiles.each do |profile|
|
79
128
|
if profile["name"] == profileName
|
80
129
|
profileId = profile['id']
|
130
|
+
break
|
81
131
|
end
|
82
132
|
end
|
83
133
|
rescue => e
|
84
|
-
raise "Something went wrong while fetching profiles: #{e.message}"
|
85
|
-
end
|
86
|
-
|
87
|
-
if profileId.nil? && !createProfileIfNotExists
|
88
|
-
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."
|
134
|
+
raise "Something went wrong while fetching profiles: #{e.message}."
|
89
135
|
end
|
90
136
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
137
|
+
return profileId
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.get_testing_group_ids(authToken, testingGroupNames)
|
141
|
+
testingGroupIds = []
|
142
|
+
remainingGroupNames = Set.new(testingGroupNames)
|
143
|
+
|
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"])
|
97
151
|
end
|
98
|
-
profileId = new_profile['id']
|
99
|
-
rescue => e
|
100
|
-
raise "Something went wrong while creating a new profile: #{e.message}"
|
101
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?
|
158
|
+
|
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."
|
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}."
|
102
199
|
end
|
103
200
|
|
104
201
|
return profileId
|
@@ -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
|
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
|
+
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: 2024-
|
11
|
+
date: 2024-12-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rest-client
|
@@ -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
|
data/images/PAT.png
DELETED
Binary file
|
Binary file
|