fastlane-plugin-appcircle_testing_distribution 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +51 -42
- data/lib/fastlane/plugin/appcircle_testing_distribution/actions/appcircle_testing_distribution_action.rb +166 -65
- data/lib/fastlane/plugin/appcircle_testing_distribution/helper/TDAuthService.rb +33 -6
- data/lib/fastlane/plugin/appcircle_testing_distribution/helper/TDUploadService.rb +184 -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 +3 -5
- 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: 213b75b8d9c150a9206153fa4bb835277992aac5ba70544ac201337a53eae712
|
|
4
|
+
data.tar.gz: 6dc6d2d960d77b3b7db6976b58aeefc17e0cfb3164bc52d137779a2f7765ae36
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3ec4a590c5418d685a2f2c7fa9b2618bf09d91d3a6bcc26252d1ec6e164b4d39a756065f0b35d886f79e75e04c0ea40a797c9de9c373ab35c9b3e3b920fdbabe
|
|
7
|
+
data.tar.gz: 0e980015ac5ee5f865da4262a1d7bd090f2c2d5cb8ce4e761ff8e8e718d61ea8822bb8c47295f1f45342d28bfaa2204923dcea78853b081a013ab21fca6aece7
|
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
|
|
|
@@ -13,7 +13,7 @@ Testing distribution is the process of distributing test builds to designated te
|
|
|
13
13
|
## Benefits of Using Testing Distribution
|
|
14
14
|
|
|
15
15
|
1. **Simplified Binary Distribution**.
|
|
16
|
-
- **Skip Traditional Stores:** Share
|
|
16
|
+
- **Skip Traditional Stores:** Share IPA, APK, AAB files directly, avoiding the need to use App Store TestFlight or Google Play Internal Testing.
|
|
17
17
|
2. **Streamlined Workflow:**
|
|
18
18
|
- **Automated Processes:** Platforms like Appcircle automate the distribution process, saving time and reducing manual effort.
|
|
19
19
|
- **Seamless Integration:** Integrates smoothly with existing DevOps pipelines, enabling efficient build and distribution workflows.
|
|
@@ -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,59 @@ 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
|
+
personalAccessKey: ENV["AC_PERSONAL_ACCESS_KEY"],
|
|
71
|
+
subOrganizationName: ENV["AC_SUB_ORGANIZATION_NAME"],
|
|
72
|
+
profileName: ENV["AC_PROFILE_NAME"],
|
|
73
|
+
createProfileIfNotExists: ENV["AC_CREATE_PROFILE_IF_NOT_EXISTS"] == "true",
|
|
74
|
+
profileCreationSettings: {
|
|
75
|
+
authType: ENV["AC_PROFILE_AUTH_TYPE"],
|
|
76
|
+
username: ENV["AC_PROFILE_USERNAME"],
|
|
77
|
+
password: ENV["AC_PROFILE_PASSWORD"],
|
|
78
|
+
testingGroupNames: ENV["AC_PROFILE_TESTING_GROUP_NAMES"]
|
|
79
|
+
},
|
|
80
|
+
appPath: ENV["AC_APP_PATH"],
|
|
81
|
+
message: ENV["AC_MESSAGE"]
|
|
87
82
|
)
|
|
88
83
|
```
|
|
89
84
|
|
|
90
|
-
|
|
91
|
-
- `profileName`: Specifies the profile that will be used for uploading the app.
|
|
92
|
-
- `createProfileIfNotExists`: Ensures that a user profile is automatically created if it does not already exist; if the profile name already exists, the app will be uploaded to that existing profile instead.
|
|
93
|
-
- `appPath`: Indicates the file path to the application that will be uploaded to Appcircle Testing Distribution Profile.
|
|
94
|
-
- `message`: Your message to testers, ensuring they receive important updates and information regarding the application.
|
|
85
|
+
### Authentication
|
|
95
86
|
|
|
96
|
-
|
|
87
|
+
Provide **either** `personalAPIToken` **or** `personalAccessKey` — not both. If neither is provided, or both are provided at the same time, the plugin fails fast with a descriptive error.
|
|
97
88
|
|
|
98
|
-
|
|
89
|
+
- `personalAPIToken`: The Appcircle Personal API token used to authenticate and authorize access to Appcircle services within this plugin.
|
|
90
|
+
- `personalAccessKey`: Alternative authentication method using a Personal Access Key. Use this if your organization provisions access keys instead of API tokens.
|
|
99
91
|
|
|
100
|
-
**
|
|
92
|
+
> **Note:** `subOrganizationName` is currently supported only when authenticating with `personalAPIToken`. When used together with `personalAccessKey`, the sub-organization switch is skipped with a warning.
|
|
101
93
|
|
|
102
|
-
|
|
94
|
+
### Other parameters
|
|
103
95
|
|
|
104
|
-
|
|
96
|
+
- `subOrganizationName` (optional): Required when the Root Organization's `personalAPIToken` is used, and you want to create the profile under a sub-organization. In this case, provide the name of the sub-organization in this field. If you directly used the sub-organization's `personalAPIToken`, this parameter is not needed.
|
|
97
|
+
- `profileName`: Specifies the profile that will be used for uploading the app.
|
|
98
|
+
- `createProfileIfNotExists` (optional): Ensures that a testing distribution profile is automatically created if it does not already exist; if the profile name already exists, the app will be uploaded to that existing profile instead.
|
|
99
|
+
- `profileCreationSettings` (optional): If `createProfileIfNotExists` is `true` and a new profile being created, the profile will be configured with these settings.
|
|
100
|
+
- `authType`: Authentication type of the profile. `none`: None, `static`: Static Username and Password, `ldap`: LDAP Login, `sso`: SSO Login.
|
|
101
|
+
- `username`: The username for the profile if authentication type set to `static` (Static Username and Password).
|
|
102
|
+
- `password`: The password for the profile if authentication type set to `static` (Static Username and Password).
|
|
103
|
+
- `testingGroupNames`: Uploaded versions will be automatically shared with these testing groups. Example format: `group1, group2, group3`.
|
|
104
|
+
- `appPath`: Indicates the file path to the application package that will be uploaded to Appcircle Testing Distribution Profile.
|
|
105
|
+
- `message`: Your message to testers, ensuring they receive important updates and information regarding the application.
|
|
106
|
+
|
|
107
|
+
## Further Details
|
|
108
|
+
|
|
109
|
+
For more information please refer to the documentation.
|
|
110
|
+
|
|
111
|
+
- [Setting Up Appcircle Testing Distribution Plugin](https://docs.appcircle.io/marketplace/fastlane/testing-distribution)
|
|
112
|
+
- [Discover Action](https://docs.appcircle.io/marketplace/fastlane/testing-distribution#discover-action)
|
|
113
|
+
- [System Requirements](https://docs.appcircle.io/marketplace/fastlane/testing-distribution#system-requirements)
|
|
114
|
+
- [User Permission Requirements](https://docs.appcircle.io/marketplace/fastlane/testing-distribution#user-permission-requirements)
|
|
115
|
+
- [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)
|
|
116
|
+
- [CLI Usage](https://docs.appcircle.io/marketplace/fastlane/testing-distribution#cli-usage)
|
|
117
|
+
- [Distributing to Sub-Organizations](https://docs.appcircle.io/marketplace/fastlane/testing-distribution#distributing-to-sub-organizations)
|
|
118
|
+
- [Leveraging Environment Variables](https://docs.appcircle.io/marketplace/fastlane/testing-distribution#leveraging-environment-variables)
|
|
119
|
+
- [References](https://docs.appcircle.io/marketplace/fastlane/testing-distribution#references)
|
|
105
120
|
|
|
106
121
|
## Issues and Feedback
|
|
107
122
|
|
|
@@ -110,9 +125,3 @@ For any other issues and feedback about this plugin, please submit it to this re
|
|
|
110
125
|
## Troubleshooting
|
|
111
126
|
|
|
112
127
|
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,74 +10,145 @@ require_relative '../helper/TDUploadService'
|
|
|
10
10
|
module Fastlane
|
|
11
11
|
module Actions
|
|
12
12
|
class AppcircleTestingDistributionAction < Action
|
|
13
|
+
VALID_EXTENSIONS = ['.apk', '.aab', '.ipa']
|
|
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
|
personalAccessKey = params[:personalAccessKey]
|
|
24
|
+
subOrganizationName = params[:subOrganizationName]
|
|
16
25
|
profileName = params[:profileName]
|
|
26
|
+
createProfileIfNotExists = params[:createProfileIfNotExists] || false
|
|
27
|
+
#
|
|
28
|
+
profileCreationSettings = params[:profileCreationSettings]
|
|
29
|
+
profileAuthType = profileCreationSettings&.dig(:authType)
|
|
30
|
+
profileUsername = profileCreationSettings&.dig(:username)
|
|
31
|
+
profilePassword = profileCreationSettings&.dig(:password)
|
|
32
|
+
profileTestingGroupNames= profileCreationSettings&.dig(:testingGroupNames)
|
|
33
|
+
#
|
|
17
34
|
appPath = params[:appPath]
|
|
18
35
|
message = params[:message]
|
|
19
|
-
createProfileIfNotExists = params[:createProfileIfNotExists]
|
|
20
36
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
file_extension = File.extname(appPath).downcase
|
|
24
|
-
unless valid_extensions.include?(file_extension)
|
|
25
|
-
UI.user_error!("Invalid file extension: #{file_extension}. For Android, use .apk or .aab. For iOS, use .ipa or .zip(.xcarchive).")
|
|
26
|
-
end
|
|
37
|
+
profileAuthType = AUTH_TYPE_MAPPING[profileAuthType] # map input to API values
|
|
27
38
|
|
|
39
|
+
# Validate auth input (either-or, not both, not none)
|
|
28
40
|
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")
|
|
41
|
+
UI.user_error!("Either Personal API Token or Personal Access Key is required to authenticate connections to Appcircle services. Please provide a valid access token or access key.")
|
|
30
42
|
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")
|
|
43
|
+
UI.user_error!("Personal API Token and Personal Access Key cannot be used together. Please provide only one authentication method.")
|
|
38
44
|
end
|
|
39
45
|
|
|
46
|
+
# Auth
|
|
47
|
+
authToken = self.ac_login(personal_api_token: personalAPIToken,
|
|
48
|
+
personal_access_key: personalAccessKey,
|
|
49
|
+
sub_organization_name: subOrganizationName)
|
|
40
50
|
|
|
41
|
-
|
|
51
|
+
# Get or create profile
|
|
52
|
+
profileId = self.ac_get_or_create_profile(authToken, profileName, createProfileIfNotExists, profileCreationSettings, profileAuthType, profileUsername, profilePassword, profileTestingGroupNames)
|
|
42
53
|
|
|
43
|
-
|
|
44
|
-
self.ac_upload(authToken, appPath, profileId, message)
|
|
54
|
+
# Upload package
|
|
55
|
+
self.ac_upload(authToken, appPath, profileId, profileName, message)
|
|
45
56
|
end
|
|
46
57
|
|
|
47
|
-
def self.ac_login(
|
|
58
|
+
def self.ac_login(personal_api_token:, personal_access_key:, sub_organization_name:)
|
|
48
59
|
begin
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
token = ''
|
|
61
|
+
|
|
62
|
+
if personal_access_key
|
|
63
|
+
user = TDAuthService.get_ac_token_with_personal_access_key(personal_access_key: personal_access_key)
|
|
51
64
|
else
|
|
52
|
-
user = TDAuthService.get_ac_token(pat:
|
|
65
|
+
user = TDAuthService.get_ac_token(pat: personal_api_token)
|
|
53
66
|
end
|
|
54
67
|
UI.success("Login is successful.")
|
|
55
|
-
|
|
68
|
+
token = user.accessToken
|
|
69
|
+
|
|
70
|
+
if sub_organization_name
|
|
71
|
+
if personal_access_key
|
|
72
|
+
UI.important("Warning: subOrganizationName is currently only supported with personalAPIToken auth. Ignoring sub-organization switch for Personal Access Key login.")
|
|
73
|
+
else
|
|
74
|
+
organization_id = TDAuthService.get_organization_id(access_token: token, name: sub_organization_name)
|
|
75
|
+
user = TDAuthService.get_ac_token(pat: personal_api_token, sub_organization_id: organization_id)
|
|
76
|
+
UI.message("Switched to sub-organization: #{sub_organization_name}")
|
|
77
|
+
token = user.accessToken
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
return token
|
|
82
|
+
|
|
56
83
|
rescue => e
|
|
57
|
-
UI.user_error!("Login failed: #{e.message}")
|
|
84
|
+
UI.user_error!("Login failed: \"#{e.message}\".")
|
|
58
85
|
end
|
|
59
86
|
end
|
|
60
|
-
|
|
87
|
+
|
|
88
|
+
def self.ac_get_or_create_profile(authToken, profileName, createProfileIfNotExists, profileCreationSettings, profileAuthType, profileUsername, profilePassword, profileTestingGroupNames)
|
|
89
|
+
begin
|
|
90
|
+
profileId = TDUploadService.get_profile_id(authToken, profileName)
|
|
91
|
+
|
|
92
|
+
if profileId
|
|
93
|
+
UI.message("Profile '#{profileName}' found with ID: #{profileId}.")
|
|
94
|
+
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
|
|
95
|
+
|
|
96
|
+
elsif profileId.nil? && !createProfileIfNotExists
|
|
97
|
+
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.")
|
|
98
|
+
elsif profileId.nil? && createProfileIfNotExists
|
|
99
|
+
UI.message("Profile '#{profileName}' not found. Creating the new profile...")
|
|
100
|
+
profileId = TDUploadService.create_profile(authToken, profileName, profileAuthType, profileUsername, profilePassword, profileTestingGroupNames)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
return profileId
|
|
104
|
+
|
|
105
|
+
rescue => e
|
|
106
|
+
UI.user_error!("Couldn't get the profile: \"#{e.message}\".")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def self.ac_upload(token, appPath, profileID, profileName, message)
|
|
111
|
+
begin
|
|
112
|
+
UI.message("Upload started.")
|
|
113
|
+
response = TDUploadService.upload_artifact(token: token, message: message, app: appPath, dist_profile_id: profileID)
|
|
114
|
+
result = self.checkTaskStatus(token, response['taskId'])
|
|
115
|
+
|
|
116
|
+
if result
|
|
117
|
+
UI.success("#{appPath} uploaded to profile '#{profileName}' successfully 🎉")
|
|
118
|
+
end
|
|
119
|
+
rescue => e
|
|
120
|
+
status_code = e.respond_to?(:response) && e.response ? e.response.code : 'unknown'
|
|
121
|
+
UI.user_error!("Upload failed with status code '#{status_code}', with message \"#{e.message}\".")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
61
125
|
def self.checkTaskStatus(authToken, taskId)
|
|
62
126
|
uri = URI.parse("https://api.appcircle.io/task/v1/tasks/#{taskId}")
|
|
63
|
-
timeout = 1
|
|
64
127
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
128
|
+
check_interval = 1
|
|
129
|
+
# timeout = 2 * 60 * 60 # 2 hours in seconds
|
|
130
|
+
# start_time = Time.now
|
|
131
|
+
|
|
132
|
+
loop do
|
|
133
|
+
response = self.send_request(uri, authToken)
|
|
134
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
135
|
+
stateValue = JSON.parse(response.body)["stateValue"]
|
|
136
|
+
|
|
137
|
+
if stateValue == 1
|
|
138
|
+
sleep(check_interval)
|
|
139
|
+
elsif stateValue == 3
|
|
140
|
+
return true
|
|
141
|
+
else
|
|
142
|
+
UI.error("Task Id #{taskId} failed with state value #{stateValue}.")
|
|
143
|
+
UI.user_error!("Upload could not be completed successfully.")
|
|
144
|
+
end
|
|
75
145
|
else
|
|
76
|
-
UI.
|
|
77
|
-
raise "Upload could not completed successfully"
|
|
146
|
+
UI.user_error!("Upload failed with response code #{response.code} and message '#{response.message}'.")
|
|
78
147
|
end
|
|
79
|
-
|
|
80
|
-
|
|
148
|
+
|
|
149
|
+
# if Time.now - start_time > timeout
|
|
150
|
+
# UI.user_error!("Task Id #{taskId} timed out after 2 hours.")
|
|
151
|
+
# end
|
|
81
152
|
end
|
|
82
153
|
end
|
|
83
154
|
|
|
@@ -89,20 +160,6 @@ module Fastlane
|
|
|
89
160
|
http.request(request)
|
|
90
161
|
end
|
|
91
162
|
|
|
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
|
-
|
|
106
163
|
def self.description
|
|
107
164
|
"Efficiently distribute application builds to users or testing groups using Appcircle's robust platform."
|
|
108
165
|
end
|
|
@@ -117,7 +174,7 @@ module Fastlane
|
|
|
117
174
|
|
|
118
175
|
def self.details
|
|
119
176
|
# Optional:
|
|
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"
|
|
177
|
+
"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."
|
|
121
178
|
end
|
|
122
179
|
|
|
123
180
|
def self.available_options
|
|
@@ -127,36 +184,80 @@ module Fastlane
|
|
|
127
184
|
description: "Provide Personal API Token to authenticate connections to Appcircle services (alternative to personalAccessKey)",
|
|
128
185
|
optional: true,
|
|
129
186
|
type: String),
|
|
130
|
-
|
|
187
|
+
|
|
131
188
|
FastlaneCore::ConfigItem.new(key: :personalAccessKey,
|
|
132
189
|
env_name: "AC_PERSONAL_ACCESS_KEY",
|
|
133
190
|
description: "Provide Personal Access Key to authenticate connections to Appcircle services (alternative to personalAPIToken)",
|
|
134
191
|
optional: true,
|
|
135
192
|
type: String),
|
|
136
|
-
|
|
193
|
+
|
|
194
|
+
FastlaneCore::ConfigItem.new(key: :subOrganizationName,
|
|
195
|
+
env_name: "AC_SUB_ORGANIZATION_NAME",
|
|
196
|
+
description: "Optional: Sub-organization name for app distribution. Profiles will be created under root organization if not provided",
|
|
197
|
+
optional: true,
|
|
198
|
+
type: String),
|
|
199
|
+
|
|
137
200
|
FastlaneCore::ConfigItem.new(key: :profileName,
|
|
138
201
|
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",
|
|
202
|
+
description: "Enter the profile name of the Appcircle testing distribution profile. This name uniquely identifies the profile under which your applications will be distributed",
|
|
140
203
|
optional: false,
|
|
141
|
-
type: String
|
|
142
|
-
|
|
204
|
+
type: String,
|
|
205
|
+
verify_block: proc do |value|
|
|
206
|
+
UI.user_error!("Profile name cannot be empty. Please provide a testing distribution profile name.") unless value && !value.empty?
|
|
207
|
+
end),
|
|
208
|
+
|
|
143
209
|
FastlaneCore::ConfigItem.new(key: :createProfileIfNotExists,
|
|
144
210
|
env_name: "AC_CREATE_PROFILE_IF_NOT_EXISTS",
|
|
145
|
-
description: "If the profile does not exist, create a new profile with the given name",
|
|
211
|
+
description: "Optional: If the profile does not exist, create a new profile with the given name",
|
|
146
212
|
optional: true,
|
|
147
213
|
type: Boolean),
|
|
148
214
|
|
|
215
|
+
FastlaneCore::ConfigItem.new(key: :profileCreationSettings,
|
|
216
|
+
description: "Optional: Profile creation settings for the testing distribution profile",
|
|
217
|
+
optional: true,
|
|
218
|
+
type: Hash,
|
|
219
|
+
verify_block: proc do |value|
|
|
220
|
+
# Parse and Validate
|
|
221
|
+
if value[:authType] && !value[:authType].empty?
|
|
222
|
+
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])
|
|
223
|
+
|
|
224
|
+
if value[:authType] == 'static'
|
|
225
|
+
UI.user_error!("username must be a String and at least 6 characters long.") unless value[:username].kind_of?(String) && value[:username].length >= 6
|
|
226
|
+
UI.user_error!("password must be a String and at least 6 characters long.") unless value[:password].kind_of?(String) && value[:password].length >= 6
|
|
227
|
+
else
|
|
228
|
+
value[:username] = nil
|
|
229
|
+
value[:password] = nil
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
if value[:testingGroupNames] && !value[:testingGroupNames].empty?
|
|
234
|
+
value[:testingGroupNames] = value[:testingGroupNames].to_s.split(",").map(&:strip)
|
|
235
|
+
UI.user_error!("testingGroupNames must be a string array. Ex: 'group1, group2, group3'.") unless value[:testingGroupNames].kind_of?(Array)
|
|
236
|
+
end
|
|
237
|
+
end),
|
|
238
|
+
|
|
149
239
|
FastlaneCore::ConfigItem.new(key: :appPath,
|
|
150
240
|
env_name: "AC_APP_PATH",
|
|
151
|
-
description: "Specify the path to your application file. For iOS, this can be a .ipa
|
|
241
|
+
description: "Specify the path to your application file. For iOS, this can be a .ipa file path. For Android, specify the .apk or .aab file path",
|
|
152
242
|
optional: false,
|
|
153
|
-
type: String
|
|
243
|
+
type: String,
|
|
244
|
+
verify_block: proc do |value|
|
|
245
|
+
UI.user_error!("Application file path cannot be empty. Please provide a valid application file path.") unless value && !value.empty?
|
|
246
|
+
|
|
247
|
+
file_extension = File.extname(value).downcase
|
|
248
|
+
unless VALID_EXTENSIONS.include?(file_extension)
|
|
249
|
+
UI.user_error!("Invalid file extension: '#{file_extension}'. For Android, use .apk or .aab. For iOS, use .ipa.")
|
|
250
|
+
end
|
|
251
|
+
end),
|
|
154
252
|
|
|
155
253
|
FastlaneCore::ConfigItem.new(key: :message,
|
|
156
254
|
env_name: "AC_MESSAGE",
|
|
157
|
-
description: "
|
|
255
|
+
description: "Message to include with the distribution to provide additional information to testers or users receiving the build",
|
|
158
256
|
optional: false,
|
|
159
|
-
type: String
|
|
257
|
+
type: String,
|
|
258
|
+
verify_block: proc do |value|
|
|
259
|
+
UI.user_error!("Message field cannot be empty. Please provide a message.") unless value && !value.empty?
|
|
260
|
+
end)
|
|
160
261
|
]
|
|
161
262
|
end
|
|
162
263
|
|
|
@@ -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
|
|
|
@@ -55,7 +82,7 @@ module TDAuthService
|
|
|
55
82
|
request = Net::HTTP::Post.new(uri)
|
|
56
83
|
request.content_type = 'application/x-www-form-urlencoded'
|
|
57
84
|
request['Accept'] = 'application/json'
|
|
58
|
-
|
|
85
|
+
|
|
59
86
|
# Encode parameters
|
|
60
87
|
params = { 'personal-access-key' => personal_access_key }
|
|
61
88
|
request.body = URI.encode_www_form(params)
|
|
@@ -75,7 +102,7 @@ module TDAuthService
|
|
|
75
102
|
|
|
76
103
|
return user
|
|
77
104
|
else
|
|
78
|
-
raise "
|
|
105
|
+
raise "Error: (#{response.code} #{response.message})."
|
|
79
106
|
end
|
|
80
107
|
end
|
|
81
108
|
end
|
|
@@ -6,20 +6,80 @@ require 'rest-client'
|
|
|
6
6
|
BASE_URL = "https://api.appcircle.io"
|
|
7
7
|
|
|
8
8
|
module TDUploadService
|
|
9
|
+
UI = FastlaneCore::UI
|
|
10
|
+
|
|
9
11
|
def self.upload_artifact(token:, message:, app:, dist_profile_id:)
|
|
10
|
-
|
|
12
|
+
file_path = app
|
|
13
|
+
file_name = File.basename(file_path)
|
|
14
|
+
file_size = File.size(file_path)
|
|
15
|
+
|
|
16
|
+
upload_info_url = "#{BASE_URL}/distribution/v1/profiles/#{dist_profile_id}/app-versions"
|
|
11
17
|
headers = {
|
|
12
18
|
Authorization: "Bearer #{token}",
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
payload = {
|
|
16
|
-
Message: message,
|
|
17
|
-
File: File.new(app, 'rb')
|
|
19
|
+
accept: 'application/json'
|
|
18
20
|
}
|
|
19
|
-
|
|
21
|
+
|
|
22
|
+
uri = URI(upload_info_url)
|
|
23
|
+
uri.query = URI.encode_www_form({
|
|
24
|
+
action: 'uploadInformation',
|
|
25
|
+
fileName: file_name,
|
|
26
|
+
fileSize: file_size
|
|
27
|
+
})
|
|
28
|
+
|
|
20
29
|
begin
|
|
21
|
-
|
|
22
|
-
|
|
30
|
+
UI.message("Getting file upload information...")
|
|
31
|
+
response = RestClient.get(uri.to_s, headers)
|
|
32
|
+
upload_info = JSON.parse(response.body)
|
|
33
|
+
if response.code.between?(200, 299)
|
|
34
|
+
UI.success("File upload information retrieved successfully with status code: #{response.code}")
|
|
35
|
+
else
|
|
36
|
+
UI.error("Failed to retrieve file upload information with status code: #{response.code}")
|
|
37
|
+
raise "Failed to retrieve file upload information."
|
|
38
|
+
end
|
|
39
|
+
file_id = upload_info['fileId']
|
|
40
|
+
upload_url = upload_info['uploadUrl']
|
|
41
|
+
|
|
42
|
+
file_content = File.binread(file_path)
|
|
43
|
+
UI.message("Uploading file to Appcircle...")
|
|
44
|
+
response = RestClient.put(
|
|
45
|
+
upload_url,
|
|
46
|
+
file_content,
|
|
47
|
+
{ content_type: 'application/octet-stream' }
|
|
48
|
+
)
|
|
49
|
+
if response.code.between?(200, 299)
|
|
50
|
+
UI.success("File upload finished successfully with status code: #{response.code}")
|
|
51
|
+
else
|
|
52
|
+
UI.error("File upload failed with status code: #{response.code}")
|
|
53
|
+
raise "File upload failed."
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
commit_url = "#{BASE_URL}/distribution/v1/profiles/#{dist_profile_id}/app-versions"
|
|
57
|
+
uri = URI(commit_url)
|
|
58
|
+
uri.query = URI.encode_www_form({ action: 'commitFileUpload' })
|
|
59
|
+
|
|
60
|
+
commit_payload = {
|
|
61
|
+
fileId: file_id,
|
|
62
|
+
fileName: file_name,
|
|
63
|
+
message: message
|
|
64
|
+
}.to_json
|
|
65
|
+
|
|
66
|
+
commit_headers = {
|
|
67
|
+
Authorization: "Bearer #{token}",
|
|
68
|
+
content_type: :json,
|
|
69
|
+
accept: 'application/json'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
UI.message("Committing file upload...")
|
|
73
|
+
commit_response = RestClient.post(uri.to_s, commit_payload, commit_headers)
|
|
74
|
+
if commit_response.code.between?(200, 299)
|
|
75
|
+
result = JSON.parse(commit_response.body)
|
|
76
|
+
UI.success("Commit successful with status code: #{commit_response.code}")
|
|
77
|
+
else
|
|
78
|
+
UI.error("Commit failed with status code: #{commit_response.code}")
|
|
79
|
+
raise "Commit failed with status code: #{commit_response.code}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
return result
|
|
23
83
|
rescue RestClient::ExceptionWithResponse => e
|
|
24
84
|
raise e
|
|
25
85
|
rescue StandardError => e
|
|
@@ -38,9 +98,26 @@ module TDUploadService
|
|
|
38
98
|
|
|
39
99
|
begin
|
|
40
100
|
response = RestClient.get(url, headers)
|
|
41
|
-
|
|
101
|
+
JSON.parse(response.body)
|
|
102
|
+
rescue RestClient::ExceptionWithResponse => e
|
|
103
|
+
raise e
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
raise e
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def self.get_testing_groups(auth_token:)
|
|
110
|
+
url = "#{BASE_URL}/distribution/v2/testing-groups"
|
|
42
111
|
|
|
43
|
-
|
|
112
|
+
# Set up the headers with authentication
|
|
113
|
+
headers = {
|
|
114
|
+
Authorization: "Bearer #{auth_token}",
|
|
115
|
+
accept: 'application/json'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
begin
|
|
119
|
+
response = RestClient.get(url, headers)
|
|
120
|
+
JSON.parse(response.body)
|
|
44
121
|
rescue RestClient::ExceptionWithResponse => e
|
|
45
122
|
raise e
|
|
46
123
|
rescue StandardError => e
|
|
@@ -69,7 +146,40 @@ module TDUploadService
|
|
|
69
146
|
end
|
|
70
147
|
end
|
|
71
148
|
|
|
72
|
-
def self.
|
|
149
|
+
def self.update_distribution_profile(profile_id:, auth_type:, username:, password:, testing_group_ids:, auth_token:)
|
|
150
|
+
url = "#{BASE_URL}/distribution/v2/profiles/#{profile_id}"
|
|
151
|
+
headers = {
|
|
152
|
+
Authorization: "Bearer #{auth_token}",
|
|
153
|
+
content_type: :json,
|
|
154
|
+
accept: 'application/json-patch+json'
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
### Construct the payload
|
|
158
|
+
payload = {}
|
|
159
|
+
|
|
160
|
+
settings_payload = {
|
|
161
|
+
authenticationType: auth_type,
|
|
162
|
+
username: username,
|
|
163
|
+
password: password
|
|
164
|
+
}.compact
|
|
165
|
+
|
|
166
|
+
payload[:settings] = settings_payload unless settings_payload.empty?
|
|
167
|
+
payload[:testingGroupIds] = testing_group_ids unless testing_group_ids&.empty?
|
|
168
|
+
|
|
169
|
+
payload = payload.compact.to_json
|
|
170
|
+
###
|
|
171
|
+
|
|
172
|
+
begin
|
|
173
|
+
response = RestClient.patch(url, payload, headers)
|
|
174
|
+
JSON.parse(response.body)
|
|
175
|
+
rescue RestClient::ExceptionWithResponse => e
|
|
176
|
+
raise e
|
|
177
|
+
rescue StandardError => e
|
|
178
|
+
raise e
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def self.get_profile_id(authToken, profileName)
|
|
73
183
|
profileId = nil
|
|
74
184
|
|
|
75
185
|
begin
|
|
@@ -77,27 +187,75 @@ module TDUploadService
|
|
|
77
187
|
profiles.each do |profile|
|
|
78
188
|
if profile["name"] == profileName
|
|
79
189
|
profileId = profile['id']
|
|
190
|
+
break
|
|
80
191
|
end
|
|
81
192
|
end
|
|
82
193
|
rescue => e
|
|
83
|
-
raise "Something went wrong while fetching profiles: #{e.message}"
|
|
84
|
-
end
|
|
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."
|
|
194
|
+
raise "Something went wrong while fetching profiles: #{e.message}."
|
|
88
195
|
end
|
|
89
196
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
197
|
+
return profileId
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def self.get_testing_group_ids(authToken, testingGroupNames)
|
|
201
|
+
testingGroupIds = []
|
|
202
|
+
remainingGroupNames = Set.new(testingGroupNames)
|
|
203
|
+
|
|
204
|
+
begin
|
|
205
|
+
groups = TDUploadService.get_testing_groups(auth_token: authToken)
|
|
206
|
+
|
|
207
|
+
groups.each do |group|
|
|
208
|
+
if remainingGroupNames.include?(group["name"])
|
|
209
|
+
testingGroupIds.push(group['id'])
|
|
210
|
+
remainingGroupNames.delete(group["name"])
|
|
96
211
|
end
|
|
97
|
-
profileId = new_profile['id']
|
|
98
|
-
rescue => e
|
|
99
|
-
raise "Something went wrong while creating a new profile: #{e.message}"
|
|
100
212
|
end
|
|
213
|
+
rescue => e
|
|
214
|
+
raise "Something went wrong while fetching testing groups: #{e.message}."
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
raise "Following testing groups couldn't be found: '#{remainingGroupNames.to_a.join(', ')}'. Aborting profile creation..." unless remainingGroupNames.empty?
|
|
218
|
+
|
|
219
|
+
return testingGroupIds
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def self.create_profile(authToken, profileName, profileAuthType, profileUsername, profilePassword, profileTestingGroupNames)
|
|
223
|
+
# Get testing group IDs
|
|
224
|
+
if !profileTestingGroupNames&.empty?
|
|
225
|
+
profileTestingGroupIds = TDUploadService.get_testing_group_ids(authToken, profileTestingGroupNames)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Create
|
|
229
|
+
begin
|
|
230
|
+
new_profile = TDUploadService.create_distribution_profile(
|
|
231
|
+
name: profileName,
|
|
232
|
+
auth_token: authToken
|
|
233
|
+
)
|
|
234
|
+
if new_profile.nil?
|
|
235
|
+
raise "Error: The new profile could not be created."
|
|
236
|
+
end
|
|
237
|
+
profileId = new_profile['id']
|
|
238
|
+
rescue => e
|
|
239
|
+
raise "Something went wrong while creating a new profile: #{e.message}."
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Configure
|
|
243
|
+
begin
|
|
244
|
+
Fastlane::UI.message("Configuring the profile...")
|
|
245
|
+
configured_profile = TDUploadService.update_distribution_profile(
|
|
246
|
+
profile_id: profileId,
|
|
247
|
+
auth_type: profileAuthType,
|
|
248
|
+
username: profileUsername,
|
|
249
|
+
password: profilePassword,
|
|
250
|
+
testing_group_ids: profileTestingGroupIds,
|
|
251
|
+
auth_token: authToken
|
|
252
|
+
)
|
|
253
|
+
if configured_profile.nil?
|
|
254
|
+
raise "Error: The new profile could not be configured."
|
|
255
|
+
end
|
|
256
|
+
profileId = configured_profile['id'] # Should be the same as before
|
|
257
|
+
rescue => e
|
|
258
|
+
raise "Something went wrong while configuring the new profile: #{e.message}."
|
|
101
259
|
end
|
|
102
260
|
|
|
103
261
|
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.
|
|
4
|
+
version: 0.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- appcircleio
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-04-30 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
|
|
@@ -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:
|
data/images/PAT.png
DELETED
|
Binary file
|
|
Binary file
|