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 +4 -4
- data/LICENSE +1 -1
- data/README.md +39 -41
- data/lib/fastlane/plugin/appcircle_testing_distribution/actions/appcircle_testing_distribution_action.rb +154 -75
- data/lib/fastlane/plugin/appcircle_testing_distribution/helper/TDAuthService.rb +21 -23
- data/lib/fastlane/plugin/appcircle_testing_distribution/helper/TDUploadService.rb +122 -26
- 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 +6 -8
- 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
|
[](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
|
-
.png>)
|
|
45
|
+
.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
|
-
.png>)
|
|
55
|
+
.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,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
|
-
|
|
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
|
-
|
|
35
|
+
|
|
36
|
+
profileAuthType = AUTH_TYPE_MAPPING[profileAuthType] # map input to API values
|
|
20
37
|
|
|
21
|
-
|
|
38
|
+
# Auth
|
|
39
|
+
authToken = self.ac_login(personalAPIToken, subOrganizationName)
|
|
22
40
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
UI.user_error!("
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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.
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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: :
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 "
|
|
45
|
+
raise "Error: (#{response.code} #{response.message})."
|
|
44
46
|
end
|
|
45
47
|
end
|
|
46
48
|
|
|
47
|
-
def self.
|
|
48
|
-
endpoint_url = 'https://
|
|
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::
|
|
53
|
-
request
|
|
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.
|
|
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 "
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if
|
|
97
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
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:
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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.
|
|
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
|