fastlane-plugin-appcircle_testing_distribution 0.4.0 → 0.4.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 +4 -4
- data/LICENSE +1 -1
- data/README.md +41 -39
- data/images/PAT.png +0 -0
- data/images/distribution-start.png +0 -0
- data/lib/fastlane/plugin/appcircle_testing_distribution/actions/appcircle_testing_distribution_action.rb +79 -153
- data/lib/fastlane/plugin/appcircle_testing_distribution/helper/TDAuthService.rb +21 -16
- data/lib/fastlane/plugin/appcircle_testing_distribution/helper/TDUploadService.rb +20 -118
- data/lib/fastlane/plugin/appcircle_testing_distribution/helper/appcircle_testing_distribution_helper.rb +3 -0
- data/lib/fastlane/plugin/appcircle_testing_distribution/version.rb +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ceb1940fa8c0d58e985bded6be73f61f64afb0599e852c52431811159af01212
|
|
4
|
+
data.tar.gz: 130705717aba452336764a57fe29a5c14c29b648e8d528e1ce3edae406e66359
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c1537e66400a4f57df3553ce8ef8fcadc89bee630557aa2eed668e7862e32327f391e0e6c280a7b01d7b9568ce3e3677608d771f089a4eb9aeb79f9ee0792405
|
|
7
|
+
data.tar.gz: f39fbbe473f79aceeac915a63fdbe9a638eb56a91172b8c7403d34367d95883a31e32f78cafb4fdb9ef3526108b534c4efb58a449f740ea906264aed08e4fb4b
|
data/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
The MIT License (MIT)
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2024
|
|
3
|
+
Copyright (c) 2024 Guven Karanfil <guven.karanfil@smartface.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
|
[](https://rubygems.org/gems/fastlane-plugin-appcircle_testing_distribution)
|
|
4
4
|
|
|
@@ -38,11 +38,24 @@ 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
|
+
## System Requirements
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
**Compatible Agents:**
|
|
44
44
|
|
|
45
|
-
|
|
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
|
+
.png>)
|
|
46
59
|
|
|
47
60
|
## Generating/Managing the Personal API Tokens
|
|
48
61
|
|
|
@@ -52,11 +65,11 @@ To generate a Personal API Token, follow these steps:
|
|
|
52
65
|
2. You'll find the Personal API Token section in the top right corner.
|
|
53
66
|
3. Press the "Generate Token" button to generate your first token.
|
|
54
67
|
|
|
55
|
-
.png>)
|
|
68
|
+
.png>)
|
|
56
69
|
|
|
57
70
|
## Getting Started with the Extension: Usage Guide
|
|
58
71
|
|
|
59
|
-
To share your builds with testers, you can create
|
|
72
|
+
To share your builds with testers, you can create distribution profiles and assign testing groups to these profiles.
|
|
60
73
|
|
|
61
74
|
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:
|
|
62
75
|
|
|
@@ -64,48 +77,31 @@ This project is a [_fastlane_](https://github.com/fastlane/fastlane) plugin. To
|
|
|
64
77
|
fastlane add_plugin appcircle_testing_distribution
|
|
65
78
|
```
|
|
66
79
|
|
|
67
|
-
```
|
|
80
|
+
```yml
|
|
68
81
|
appcircle_testing_distribution(
|
|
69
|
-
personalAPIToken:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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"]
|
|
82
|
+
personalAPIToken: "$(AC_PERSONAL_API_TOKEN)",
|
|
83
|
+
profileName: "$(AC_PROFILE_NAME)",
|
|
84
|
+
createProfileIfNotExists: Boolean,
|
|
85
|
+
appPath: "$(AC_APP_PATH)",
|
|
86
|
+
message: "$(AC_MESSAGE)",
|
|
81
87
|
)
|
|
82
88
|
```
|
|
83
89
|
|
|
84
|
-
- `personalAPIToken`: The Appcircle Personal API token
|
|
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.
|
|
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.
|
|
86
91
|
- `profileName`: Specifies the profile that will be used for uploading the app.
|
|
87
|
-
- `createProfileIfNotExists
|
|
88
|
-
- `
|
|
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.
|
|
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.
|
|
94
94
|
- `message`: Your message to testers, ensuring they receive important updates and information regarding the application.
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
### Leveraging Environment Variables
|
|
97
97
|
|
|
98
|
-
|
|
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.
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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)
|
|
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)
|
|
109
105
|
|
|
110
106
|
## Issues and Feedback
|
|
111
107
|
|
|
@@ -114,3 +110,9 @@ For any other issues and feedback about this plugin, please submit it to this re
|
|
|
114
110
|
## Troubleshooting
|
|
115
111
|
|
|
116
112
|
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)
|
data/images/PAT.png
ADDED
|
Binary file
|
|
Binary file
|
|
@@ -10,127 +10,74 @@ 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
|
-
|
|
21
13
|
def self.run(params)
|
|
22
14
|
personalAPIToken = params[:personalAPIToken]
|
|
23
|
-
|
|
15
|
+
personalAccessKey = params[:personalAccessKey]
|
|
24
16
|
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
|
-
#
|
|
33
17
|
appPath = params[:appPath]
|
|
34
18
|
message = params[:message]
|
|
35
|
-
|
|
36
|
-
profileAuthType = AUTH_TYPE_MAPPING[profileAuthType] # map input to API values
|
|
37
|
-
|
|
38
|
-
# Auth
|
|
39
|
-
authToken = self.ac_login(personalAPIToken, subOrganizationName)
|
|
40
|
-
|
|
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
|
|
19
|
+
createProfileIfNotExists = params[:createProfileIfNotExists]
|
|
47
20
|
|
|
48
|
-
|
|
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
|
|
21
|
+
valid_extensions = ['.apk', '.aab', '.ipa', '.zip']
|
|
64
22
|
|
|
65
|
-
|
|
66
|
-
|
|
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).")
|
|
67
26
|
end
|
|
68
|
-
end
|
|
69
27
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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")
|
|
38
|
+
end
|
|
73
39
|
|
|
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
40
|
|
|
78
|
-
|
|
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
|
+
authToken = self.ac_login(personalAPIToken, personalAccessKey)
|
|
84
42
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
rescue => e
|
|
88
|
-
UI.user_error!("Couldn't get the profile: \"#{e.message}\".")
|
|
89
|
-
end
|
|
43
|
+
profileId = TDUploadService.get_profile_id(authToken, profileName, createProfileIfNotExists)
|
|
44
|
+
self.ac_upload(authToken, appPath, profileId, message)
|
|
90
45
|
end
|
|
91
46
|
|
|
92
|
-
def self.
|
|
47
|
+
def self.ac_login(personalAPIToken, personalAccessKey)
|
|
93
48
|
begin
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if result
|
|
99
|
-
UI.success("#{appPath} uploaded to profile '#{profileName}' successfully 🎉")
|
|
49
|
+
if personalAccessKey
|
|
50
|
+
user = TDAuthService.get_ac_token_with_personal_access_key(personal_access_key: personalAccessKey)
|
|
51
|
+
else
|
|
52
|
+
user = TDAuthService.get_ac_token(pat: personalAPIToken)
|
|
100
53
|
end
|
|
54
|
+
UI.success("Login is successful.")
|
|
55
|
+
return user.accessToken
|
|
101
56
|
rescue => e
|
|
102
|
-
|
|
103
|
-
UI.user_error!("Upload failed with status code '#{status_code}', with message \"#{e.message}\".")
|
|
57
|
+
UI.user_error!("Login failed: #{e.message}")
|
|
104
58
|
end
|
|
105
59
|
end
|
|
106
|
-
|
|
60
|
+
|
|
107
61
|
def self.checkTaskStatus(authToken, taskId)
|
|
108
62
|
uri = URI.parse("https://api.appcircle.io/task/v1/tasks/#{taskId}")
|
|
63
|
+
timeout = 1
|
|
109
64
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
65
|
+
response = self.send_request(uri, authToken)
|
|
66
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
67
|
+
stateValue = JSON.parse(response.body)["stateValue"]
|
|
68
|
+
|
|
69
|
+
if stateValue == 1
|
|
70
|
+
sleep(1)
|
|
71
|
+
return checkTaskStatus(authToken, taskId)
|
|
72
|
+
end
|
|
73
|
+
if stateValue == 3
|
|
74
|
+
return true
|
|
127
75
|
else
|
|
128
|
-
UI.
|
|
76
|
+
UI.error("Task Id #{taskId} failed with state value #{stateValue}")
|
|
77
|
+
raise "Upload could not completed successfully"
|
|
129
78
|
end
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
# UI.user_error!("Task Id #{taskId} timed out after 2 hours.")
|
|
133
|
-
# end
|
|
79
|
+
else
|
|
80
|
+
raise "Upload failed with response code #{response.code} and message '#{response.message}'"
|
|
134
81
|
end
|
|
135
82
|
end
|
|
136
83
|
|
|
@@ -142,6 +89,20 @@ module Fastlane
|
|
|
142
89
|
http.request(request)
|
|
143
90
|
end
|
|
144
91
|
|
|
92
|
+
def self.ac_upload(token, appPath, profileID, message)
|
|
93
|
+
begin
|
|
94
|
+
response = TDUploadService.upload_artifact(token: token, message: message, app: appPath, dist_profile_id: profileID)
|
|
95
|
+
result = self.checkTaskStatus(token, response['taskId'])
|
|
96
|
+
|
|
97
|
+
if result
|
|
98
|
+
UI.success("#{appPath} Uploaded to profile id #{profileID} successfully 🎉")
|
|
99
|
+
end
|
|
100
|
+
rescue => e
|
|
101
|
+
status_code = e.respond_to?(:response) && e.response ? e.response.code : 'unknown'
|
|
102
|
+
UI.user_error!("Upload failed with status code #{status_code}, with message '#{e.message}'")
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
145
106
|
def self.description
|
|
146
107
|
"Efficiently distribute application builds to users or testing groups using Appcircle's robust platform."
|
|
147
108
|
end
|
|
@@ -156,81 +117,46 @@ module Fastlane
|
|
|
156
117
|
|
|
157
118
|
def self.details
|
|
158
119
|
# Optional:
|
|
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
|
|
120
|
+
"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"
|
|
160
121
|
end
|
|
161
122
|
|
|
162
123
|
def self.available_options
|
|
163
124
|
[
|
|
164
125
|
FastlaneCore::ConfigItem.new(key: :personalAPIToken,
|
|
165
|
-
|
|
166
|
-
|
|
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",
|
|
126
|
+
env_name: "AC_PERSONAL_API_TOKEN",
|
|
127
|
+
description: "Provide Personal API Token to authenticate connections to Appcircle services (alternative to personalAccessKey)",
|
|
174
128
|
optional: true,
|
|
175
129
|
type: String),
|
|
176
|
-
|
|
130
|
+
|
|
131
|
+
FastlaneCore::ConfigItem.new(key: :personalAccessKey,
|
|
132
|
+
env_name: "AC_PERSONAL_ACCESS_KEY",
|
|
133
|
+
description: "Provide Personal Access Key to authenticate connections to Appcircle services (alternative to personalAPIToken)",
|
|
134
|
+
optional: true,
|
|
135
|
+
type: String),
|
|
136
|
+
|
|
177
137
|
FastlaneCore::ConfigItem.new(key: :profileName,
|
|
178
|
-
|
|
138
|
+
env_name: "AC_PROFILE_NAME",
|
|
139
|
+
description: "Enter the profile name of the Appcircle distribution profile. This name uniquely identifies the profile under which your applications will be distributed",
|
|
179
140
|
optional: false,
|
|
180
|
-
type: String,
|
|
181
|
-
|
|
182
|
-
UI.user_error!("Profile name cannot be empty. Please provide a testing distribution profile name.") unless value && !value.empty?
|
|
183
|
-
end),
|
|
184
|
-
|
|
141
|
+
type: String),
|
|
142
|
+
|
|
185
143
|
FastlaneCore::ConfigItem.new(key: :createProfileIfNotExists,
|
|
186
|
-
|
|
144
|
+
env_name: "AC_CREATE_PROFILE_IF_NOT_EXISTS",
|
|
145
|
+
description: "If the profile does not exist, create a new profile with the given name",
|
|
187
146
|
optional: true,
|
|
188
147
|
type: Boolean),
|
|
189
148
|
|
|
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
|
-
|
|
214
149
|
FastlaneCore::ConfigItem.new(key: :appPath,
|
|
150
|
+
env_name: "AC_APP_PATH",
|
|
215
151
|
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",
|
|
216
152
|
optional: false,
|
|
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),
|
|
153
|
+
type: String),
|
|
226
154
|
|
|
227
155
|
FastlaneCore::ConfigItem.new(key: :message,
|
|
228
|
-
|
|
156
|
+
env_name: "AC_MESSAGE",
|
|
157
|
+
description: "Optional message to include with the distribution to provide additional information to testers or users receiving the build",
|
|
229
158
|
optional: false,
|
|
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)
|
|
159
|
+
type: String)
|
|
234
160
|
]
|
|
235
161
|
end
|
|
236
162
|
|
|
@@ -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:)
|
|
17
17
|
endpoint_url = 'https://auth.appcircle.io/auth/v2/token'
|
|
18
18
|
uri = URI(endpoint_url)
|
|
19
19
|
|
|
@@ -24,7 +24,6 @@ module TDAuthService
|
|
|
24
24
|
|
|
25
25
|
# Encode parameters
|
|
26
26
|
params = { pat: pat }
|
|
27
|
-
params[:subOrganizationId] = sub_organization_id if sub_organization_id
|
|
28
27
|
request.body = URI.encode_www_form(params)
|
|
29
28
|
|
|
30
29
|
# Make the HTTP request
|
|
@@ -32,6 +31,8 @@ module TDAuthService
|
|
|
32
31
|
http.request(request)
|
|
33
32
|
end
|
|
34
33
|
|
|
34
|
+
|
|
35
|
+
|
|
35
36
|
# Check response
|
|
36
37
|
if response.is_a?(Net::HTTPSuccess)
|
|
37
38
|
response_data = JSON.parse(response.body)
|
|
@@ -42,35 +43,39 @@ module TDAuthService
|
|
|
42
43
|
|
|
43
44
|
return user
|
|
44
45
|
else
|
|
45
|
-
raise "
|
|
46
|
+
raise "HTTP Request failed (#{response.code} #{response.message})"
|
|
46
47
|
end
|
|
47
48
|
end
|
|
48
49
|
|
|
49
|
-
def self.
|
|
50
|
-
endpoint_url = 'https://
|
|
50
|
+
def self.get_ac_token_with_personal_access_key(personal_access_key:)
|
|
51
|
+
endpoint_url = 'https://auth.appcircle.io/auth/v1/token'
|
|
51
52
|
uri = URI(endpoint_url)
|
|
52
|
-
|
|
53
|
+
|
|
53
54
|
# Create HTTP request
|
|
54
|
-
request = Net::HTTP::
|
|
55
|
-
request
|
|
55
|
+
request = Net::HTTP::Post.new(uri)
|
|
56
|
+
request.content_type = 'application/x-www-form-urlencoded'
|
|
56
57
|
request['Accept'] = 'application/json'
|
|
57
|
-
|
|
58
|
+
|
|
59
|
+
# Encode parameters
|
|
60
|
+
params = { 'personal-access-key' => personal_access_key }
|
|
61
|
+
request.body = URI.encode_www_form(params)
|
|
62
|
+
|
|
58
63
|
# Make the HTTP request
|
|
59
64
|
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
60
65
|
http.request(request)
|
|
61
66
|
end
|
|
62
|
-
|
|
67
|
+
|
|
63
68
|
# Check response
|
|
64
69
|
if response.is_a?(Net::HTTPSuccess)
|
|
65
70
|
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
71
|
|
|
72
|
+
user = UserResponse.new(
|
|
73
|
+
accessToken: response_data['access_token']
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return user
|
|
72
77
|
else
|
|
73
|
-
raise "
|
|
78
|
+
raise "HTTP Request failed (#{response.code} #{response.message})"
|
|
74
79
|
end
|
|
75
80
|
end
|
|
76
81
|
end
|
|
@@ -9,12 +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}"
|
|
12
|
+
Authorization: "Bearer #{token}",
|
|
13
|
+
content_type: :multipart # multipart/form-data
|
|
13
14
|
}
|
|
14
15
|
payload = {
|
|
15
16
|
Message: message,
|
|
16
|
-
File: File.new(app, 'rb')
|
|
17
|
-
multipart: true # Force multipart encoding for RestClient
|
|
17
|
+
File: File.new(app, 'rb')
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
begin
|
|
@@ -38,26 +38,9 @@ module TDUploadService
|
|
|
38
38
|
|
|
39
39
|
begin
|
|
40
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
|
|
48
|
-
|
|
49
|
-
def self.get_testing_groups(auth_token:)
|
|
50
|
-
url = "#{BASE_URL}/distribution/v2/testing-groups"
|
|
41
|
+
parsed_response = JSON.parse(response.body)
|
|
51
42
|
|
|
52
|
-
|
|
53
|
-
headers = {
|
|
54
|
-
Authorization: "Bearer #{auth_token}",
|
|
55
|
-
accept: 'application/json'
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
begin
|
|
59
|
-
response = RestClient.get(url, headers)
|
|
60
|
-
JSON.parse(response.body)
|
|
43
|
+
parsed_response
|
|
61
44
|
rescue RestClient::ExceptionWithResponse => e
|
|
62
45
|
raise e
|
|
63
46
|
rescue StandardError => e
|
|
@@ -86,40 +69,7 @@ module TDUploadService
|
|
|
86
69
|
end
|
|
87
70
|
end
|
|
88
71
|
|
|
89
|
-
def self.
|
|
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)
|
|
72
|
+
def self.get_profile_id(authToken, profileName, createProfileIfNotExists)
|
|
123
73
|
profileId = nil
|
|
124
74
|
|
|
125
75
|
begin
|
|
@@ -127,75 +77,27 @@ module TDUploadService
|
|
|
127
77
|
profiles.each do |profile|
|
|
128
78
|
if profile["name"] == profileName
|
|
129
79
|
profileId = profile['id']
|
|
130
|
-
break
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
rescue => e
|
|
134
|
-
raise "Something went wrong while fetching profiles: #{e.message}."
|
|
135
|
-
end
|
|
136
|
-
|
|
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"])
|
|
151
80
|
end
|
|
152
81
|
end
|
|
153
82
|
rescue => e
|
|
154
|
-
raise "Something went wrong while fetching
|
|
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)
|
|
83
|
+
raise "Something went wrong while fetching profiles: #{e.message}"
|
|
166
84
|
end
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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}."
|
|
85
|
+
|
|
86
|
+
if profileId.nil? && !createProfileIfNotExists
|
|
87
|
+
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."
|
|
180
88
|
end
|
|
181
89
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
)
|
|
193
|
-
if configured_profile.nil?
|
|
194
|
-
raise "Error: The new profile could not be configured."
|
|
90
|
+
if profileId.nil? && createProfileIfNotExists
|
|
91
|
+
begin
|
|
92
|
+
puts "The test profile '#{profileName}' could not be found. A new profile is being created..."
|
|
93
|
+
new_profile = TDUploadService.create_distribution_profile(name: profileName, auth_token: authToken)
|
|
94
|
+
if new_profile.nil?
|
|
95
|
+
raise "Error: The new profile could not be created."
|
|
96
|
+
end
|
|
97
|
+
profileId = new_profile['id']
|
|
98
|
+
rescue => e
|
|
99
|
+
raise "Something went wrong while creating a new profile: #{e.message}"
|
|
195
100
|
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}."
|
|
199
101
|
end
|
|
200
102
|
|
|
201
103
|
return profileId
|
|
@@ -8,6 +8,9 @@ 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
|
|
11
14
|
end
|
|
12
15
|
end
|
|
13
16
|
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.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- appcircleio
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2025-11-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rest-client
|
|
@@ -32,6 +32,8 @@ extra_rdoc_files: []
|
|
|
32
32
|
files:
|
|
33
33
|
- LICENSE
|
|
34
34
|
- README.md
|
|
35
|
+
- images/PAT.png
|
|
36
|
+
- images/distribution-start.png
|
|
35
37
|
- images/extension-icon.png
|
|
36
38
|
- lib/fastlane/plugin/appcircle_testing_distribution.rb
|
|
37
39
|
- lib/fastlane/plugin/appcircle_testing_distribution/actions/appcircle_testing_distribution_action.rb
|
|
@@ -43,7 +45,7 @@ homepage: https://github.com/appcircleio/fastlane_plugin_appcircle_testing_distr
|
|
|
43
45
|
licenses:
|
|
44
46
|
- MIT
|
|
45
47
|
metadata:
|
|
46
|
-
rubygems_mfa_required: '
|
|
48
|
+
rubygems_mfa_required: 'true'
|
|
47
49
|
post_install_message:
|
|
48
50
|
rdoc_options: []
|
|
49
51
|
require_paths:
|
|
@@ -59,7 +61,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
59
61
|
- !ruby/object:Gem::Version
|
|
60
62
|
version: '0'
|
|
61
63
|
requirements: []
|
|
62
|
-
rubygems_version: 3.4.
|
|
64
|
+
rubygems_version: 3.4.10
|
|
63
65
|
signing_key:
|
|
64
66
|
specification_version: 4
|
|
65
67
|
summary: Efficiently distribute application builds to users or testing groups using
|