fastlane-plugin-appcircle_publish 0.1.0.beta.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +148 -0
- data/lib/fastlane/plugin/appcircle_publish/actions/appcircle_publish_action.rb +290 -0
- data/lib/fastlane/plugin/appcircle_publish/helper/appcircle_publish_helper.rb +16 -0
- data/lib/fastlane/plugin/appcircle_publish/helper/auth_service.rb +78 -0
- data/lib/fastlane/plugin/appcircle_publish/helper/upload_service.rb +149 -0
- data/lib/fastlane/plugin/appcircle_publish/version.rb +5 -0
- data/lib/fastlane/plugin/appcircle_publish.rb +16 -0
- metadata +65 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d2a731169ee6e19ffa2c1faf2d552cf6464abff3ed839b4231d6de7ed6587897
|
|
4
|
+
data.tar.gz: 346b250fbba4003f3c0b22002eebffbc9b750cce16b2fbb1c9c1b7d2fcd4b89c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: ec418da783e2feb28ff2cdf437bb8815523ae9476d78e823f1609290e684277f24af3e164775cc2eb3bc0695ed977f67a8d6e92f0d594974b316322683c12d2c
|
|
7
|
+
data.tar.gz: 2d4b73ea36345d73630c1d14318365627c768ff2dd4d5c8889e37f682f96fa4871199fb9c2ef3f26bbd5e7abd898dbd12d6e90bcd8a8802bbe8b4431c8254dc6
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Burak Yıldırım <buraky@appcircle.io>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
## Appcircle Publish
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/fastlane-plugin-appcircle_publish)
|
|
4
|
+
|
|
5
|
+
Upload an application binary to an Appcircle **Publish** profile and/or trigger
|
|
6
|
+
its publish flow (app store publishing) directly from your Fastlane pipeline.
|
|
7
|
+
|
|
8
|
+
Learn more about [Appcircle Publish](https://appcircle.io/publish-to-stores?utm_source=fastlane&utm_medium=plugin&utm_campaign=publish).
|
|
9
|
+
|
|
10
|
+
## System Requirements
|
|
11
|
+
|
|
12
|
+
**Compatible Agents:**
|
|
13
|
+
|
|
14
|
+
- macOS 14.2, 14.5
|
|
15
|
+
|
|
16
|
+
**Supported Version:**
|
|
17
|
+
|
|
18
|
+
- Fastlane 2.222.0
|
|
19
|
+
- Ruby 3.2.2
|
|
20
|
+
|
|
21
|
+
Note: Both **Appcircle Cloud** and **self-hosted** Appcircle installations are supported. See [Self-Hosted Appcircle](#self-hosted-appcircle) below to configure custom endpoints.
|
|
22
|
+
|
|
23
|
+
### Generating/Managing the Personal API Tokens
|
|
24
|
+
|
|
25
|
+
To generate a Personal API Token:
|
|
26
|
+
|
|
27
|
+
1. Go to the My Organization screen (second option at the bottom left).
|
|
28
|
+
2. Find the Personal API Token section in the top right corner.
|
|
29
|
+
3. Press the "Generate Token" button to generate your first token.
|
|
30
|
+
|
|
31
|
+
.png>)
|
|
32
|
+
|
|
33
|
+
## What the action does
|
|
34
|
+
|
|
35
|
+
The action has two independent switches — `upload` and `publish` — both default
|
|
36
|
+
to `false`. **You must enable at least one.** Create the Publish profile in
|
|
37
|
+
Appcircle first; the action targets it by name (profile names are unique per
|
|
38
|
+
platform).
|
|
39
|
+
|
|
40
|
+
| `upload` | `publish` | Behavior |
|
|
41
|
+
|:--------:|:---------:|----------|
|
|
42
|
+
| `true` | `false` | Upload `appPath` as a new app version on the profile. |
|
|
43
|
+
| `false` | `true` | Trigger the publish flow for the profile's **current release candidate**. |
|
|
44
|
+
| `true` | `true` | Upload `appPath`, **mark the new version as release candidate**, then trigger the publish flow for it. |
|
|
45
|
+
| `false` | `false` | Error — nothing to do. |
|
|
46
|
+
|
|
47
|
+
**Rules:**
|
|
48
|
+
|
|
49
|
+
- **In-progress guard:** if a publish is already running for the target profile, the action does **not** start a new one and fails fast.
|
|
50
|
+
- **Release candidate:** when both `upload` and `publish` are `true`, the freshly uploaded version is automatically marked as the release candidate before it is published. In publish-only mode, the profile's existing release candidate is published.
|
|
51
|
+
- **Progress:** while publishing, the action polls the publish status and prints step-by-step progress (with status icons) until the flow succeeds or fails.
|
|
52
|
+
|
|
53
|
+
> **Manual-approval steps:** if the profile's publish flow contains a manual step (e.g. "Get Approval via Email"), the flow waits for that action and the plugin keeps polling until it completes or the poll times out. For CI use, prefer publish flows without manual gates.
|
|
54
|
+
|
|
55
|
+
### Getting Started
|
|
56
|
+
|
|
57
|
+
This project is a [_fastlane_](https://github.com/fastlane/fastlane) plugin. To get started with `appcircle_publish`, add it to your project by running:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
fastlane add_plugin appcircle_publish
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Upload only:**
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
lane :upload_to_publish do
|
|
67
|
+
appcircle_publish(
|
|
68
|
+
personalAPIToken: "$(AC_PERSONAL_API_TOKEN)",
|
|
69
|
+
platform: "$(AC_PLATFORM)", # "ios" or "android"
|
|
70
|
+
publishProfile: "$(AC_PUBLISH_PROFILE)",
|
|
71
|
+
upload: true,
|
|
72
|
+
appPath: "$(AC_APP_PATH)"
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Publish only (publishes the profile's current release candidate):**
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
lane :trigger_publish do
|
|
81
|
+
appcircle_publish(
|
|
82
|
+
personalAPIToken: "$(AC_PERSONAL_API_TOKEN)",
|
|
83
|
+
platform: "$(AC_PLATFORM)",
|
|
84
|
+
publishProfile: "$(AC_PUBLISH_PROFILE)",
|
|
85
|
+
publish: true
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Upload and publish (uploads, marks it release candidate, then publishes):**
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
lane :upload_and_publish do
|
|
94
|
+
appcircle_publish(
|
|
95
|
+
personalAPIToken: "$(AC_PERSONAL_API_TOKEN)",
|
|
96
|
+
platform: "$(AC_PLATFORM)",
|
|
97
|
+
publishProfile: "$(AC_PUBLISH_PROFILE)",
|
|
98
|
+
upload: true,
|
|
99
|
+
publish: true,
|
|
100
|
+
appPath: "$(AC_APP_PATH)"
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- `personalAPIToken` / `personalAccessKey`: Provide exactly one to authenticate Appcircle services.
|
|
106
|
+
- `platform`: Target platform of the Publish profile — `ios` or `android`.
|
|
107
|
+
- `publishProfile`: Name of the Publish profile to target.
|
|
108
|
+
- `upload` (default `false`): Upload `appPath` as a new app version.
|
|
109
|
+
- `publish` (default `false`): Trigger the profile's publish flow.
|
|
110
|
+
- `appPath`: Path to the application file. Required when `upload` is `true`. For iOS use a `.ipa` file; for Android use a `.apk` or `.aab` file.
|
|
111
|
+
|
|
112
|
+
### Self-Hosted Appcircle
|
|
113
|
+
|
|
114
|
+
If you run a self-hosted Appcircle installation, point the action to your own endpoints with the optional `authEndpoint` and `apiEndpoint` parameters. Both default to the Appcircle cloud, so existing cloud users do not need to set them.
|
|
115
|
+
|
|
116
|
+
- `authEndpoint` (optional): Authentication endpoint URL. Defaults to `https://auth.appcircle.io`.
|
|
117
|
+
- `apiEndpoint` (optional): API endpoint URL. Defaults to `https://api.appcircle.io`.
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
appcircle_publish(
|
|
121
|
+
personalAPIToken: "$(AC_PERSONAL_API_TOKEN)",
|
|
122
|
+
platform: "$(AC_PLATFORM)",
|
|
123
|
+
publishProfile: "$(AC_PUBLISH_PROFILE)",
|
|
124
|
+
publish: true,
|
|
125
|
+
authEndpoint: "https://auth.my-appcircle.example.com",
|
|
126
|
+
apiEndpoint: "https://api.my-appcircle.example.com"
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
> **Self-signed or private CA certificates:** If your self-hosted Appcircle server uses a self-signed certificate (or one issued by a private/internal CA), requests will fail certificate validation. The plugin does not disable TLS verification. Trust the server's CA on the machine running Fastlane — add it to the system certificate store, or point the `SSL_CERT_FILE` environment variable at a PEM bundle that includes it.
|
|
131
|
+
|
|
132
|
+
### Leveraging Environment Variables
|
|
133
|
+
|
|
134
|
+
Utilize environment variables seamlessly by substituting the parameters with $(VARIABLE_NAME) in your task inputs. The plugin automatically retrieves values from the specified environment variables within your pipeline.
|
|
135
|
+
|
|
136
|
+
If you would like to learn more about this plugin and how to utilize it in your projects, please [contact us](https://appcircle.io/contact?utm_source=fastlane&utm_medium=plugin&utm_campaign=publish)
|
|
137
|
+
|
|
138
|
+
## Issues and Feedback
|
|
139
|
+
|
|
140
|
+
For any other issues and feedback about this plugin, please submit it to this repository.
|
|
141
|
+
|
|
142
|
+
## Troubleshooting
|
|
143
|
+
|
|
144
|
+
If you have trouble using plugins, check out the [Plugins Troubleshooting](https://docs.fastlane.tools/plugins/plugins-troubleshooting/) guide.
|
|
145
|
+
|
|
146
|
+
### Reference
|
|
147
|
+
|
|
148
|
+
For more detailed instructions and support, visit the [Appcircle Publish documentation](https://docs.appcircle.io/publish-to-stores-module).
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
require 'fastlane/action'
|
|
2
|
+
require 'net/http'
|
|
3
|
+
require 'uri'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
require 'fastlane/action'
|
|
7
|
+
require_relative '../helper/appcircle_publish_helper'
|
|
8
|
+
|
|
9
|
+
require_relative '../helper/auth_service'
|
|
10
|
+
require_relative '../helper/upload_service'
|
|
11
|
+
|
|
12
|
+
module Fastlane
|
|
13
|
+
module Actions
|
|
14
|
+
class AppcirclePublishAction < Action
|
|
15
|
+
@@apiToken = nil
|
|
16
|
+
@@authEndpoint = "https://auth.appcircle.io"
|
|
17
|
+
@@apiEndpoint = "https://api.appcircle.io"
|
|
18
|
+
|
|
19
|
+
FLOW_STEP_STATUS = {
|
|
20
|
+
0 => 'Success', 1 => 'Failed', 2 => 'Cancelled', 3 => 'Timeout',
|
|
21
|
+
90 => 'Waiting', 91 => 'Running', 92 => 'Completing', 99 => 'Unknown',
|
|
22
|
+
100 => 'Skipped', 200 => 'Not Started', 201 => 'Stopped',
|
|
23
|
+
202 => 'In Progress', 203 => 'Awaiting Response'
|
|
24
|
+
}.freeze
|
|
25
|
+
TERMINAL_STEP_STATUSES = [0, 1, 2, 3, 100, 201].freeze
|
|
26
|
+
ACTIVE_STEP_STATUSES = [91, 92, 202].freeze
|
|
27
|
+
|
|
28
|
+
def self.run(params)
|
|
29
|
+
personalAPIToken = params[:personalAPIToken]
|
|
30
|
+
personalAccessKey = params[:personalAccessKey]
|
|
31
|
+
@@authEndpoint = params[:authEndpoint]
|
|
32
|
+
@@apiEndpoint = params[:apiEndpoint]
|
|
33
|
+
platform = params[:platform] && params[:platform].downcase
|
|
34
|
+
publishProfile = params[:publishProfile]
|
|
35
|
+
appPath = params[:appPath]
|
|
36
|
+
upload = params[:upload]
|
|
37
|
+
publish = params[:publish]
|
|
38
|
+
|
|
39
|
+
# --- Validation ---
|
|
40
|
+
if !upload && !publish
|
|
41
|
+
UI.user_error!("Nothing to do: set 'upload' and/or 'publish' to true.")
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if personalAPIToken.nil? && personalAccessKey.nil?
|
|
45
|
+
UI.user_error!("Please provide either Personal API Token (personalAPIToken) or Personal Access Key (personalAccessKey) to authenticate connections to Appcircle services")
|
|
46
|
+
elsif !personalAPIToken.nil? && !personalAccessKey.nil?
|
|
47
|
+
UI.user_error!("Please provide only one authentication method: either Personal API Token (personalAPIToken) or Personal Access Key (personalAccessKey), not both")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if platform.nil? || (platform != "ios" && platform != "android")
|
|
51
|
+
UI.user_error!("Please provide a valid platform for the Publish profile: 'ios' or 'android'")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if publishProfile.nil?
|
|
55
|
+
UI.user_error!("Please provide the name of the Publish profile to target")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
if upload
|
|
59
|
+
UI.user_error!("Please specify 'appPath' when 'upload' is true. For iOS use a .ipa file; for Android use a .apk or .aab file") if appPath.nil?
|
|
60
|
+
valid_extensions = ['.apk', '.aab', '.ipa']
|
|
61
|
+
file_extension = File.extname(appPath).downcase
|
|
62
|
+
unless valid_extensions.include?(file_extension)
|
|
63
|
+
raise "Invalid file extension: #{file_extension}. For Android, use .apk or .aab. For iOS, use .ipa."
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# --- Auth + profile ---
|
|
68
|
+
if personalAPIToken.nil?
|
|
69
|
+
self.ac_login_with_pak(personalAccessKey)
|
|
70
|
+
else
|
|
71
|
+
self.ac_login_with_pat(personalAPIToken)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
profileId = UploadService.get_publish_profile_id(auth_token: @@apiToken, platform: platform, profile_name: publishProfile, api_endpoint: @@apiEndpoint)
|
|
75
|
+
|
|
76
|
+
# Guard: never start a new publish if one is already running for the profile.
|
|
77
|
+
if publish
|
|
78
|
+
active = UploadService.get_active_publish_count_for_profile(auth_token: @@apiToken, publish_profile_id: profileId, api_endpoint: @@apiEndpoint)
|
|
79
|
+
if active > 0
|
|
80
|
+
UI.user_error!("A publish is already in progress for profile '#{publishProfile}'. Not starting a new one.")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
appVersionId = nil
|
|
85
|
+
|
|
86
|
+
# --- Upload ---
|
|
87
|
+
if upload
|
|
88
|
+
response = UploadService.upload_artifact(token: @@apiToken, app: appPath, platform: platform, publish_profile_id: profileId, api_endpoint: @@apiEndpoint)
|
|
89
|
+
self.checkTaskStatus(response["taskId"])
|
|
90
|
+
appVersionId = UploadService.get_latest_app_version_id(auth_token: @@apiToken, platform: platform, publish_profile_id: profileId, api_endpoint: @@apiEndpoint)
|
|
91
|
+
UI.success("#{appPath} uploaded to the Appcircle Publish profile '#{publishProfile}' successfully")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# --- Publish ---
|
|
95
|
+
if publish
|
|
96
|
+
if upload && appVersionId
|
|
97
|
+
UploadService.mark_release_candidate(auth_token: @@apiToken, platform: platform, publish_profile_id: profileId, app_version_id: appVersionId, api_endpoint: @@apiEndpoint)
|
|
98
|
+
UI.message("Marked the uploaded version as release candidate.")
|
|
99
|
+
else
|
|
100
|
+
appVersionId = UploadService.get_release_candidate_version_id(auth_token: @@apiToken, platform: platform, publish_profile_id: profileId, api_endpoint: @@apiEndpoint)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
publishId = UploadService.get_publish_id(auth_token: @@apiToken, platform: platform, publish_profile_id: profileId, app_version_id: appVersionId, api_endpoint: @@apiEndpoint)
|
|
104
|
+
UploadService.start_publish(auth_token: @@apiToken, platform: platform, publish_profile_id: profileId, publish_id: publishId, api_endpoint: @@apiEndpoint)
|
|
105
|
+
UI.success("Publish flow started for profile '#{publishProfile}'.")
|
|
106
|
+
success = self.poll_publish_status(platform, profileId, appVersionId)
|
|
107
|
+
UI.user_error!("Publish flow failed.") unless success
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.ac_login_with_pat(accessToken)
|
|
112
|
+
user = AuthService.get_ac_token(pat: accessToken, auth_endpoint: @@authEndpoint)
|
|
113
|
+
UI.success("Login is successful.")
|
|
114
|
+
@@apiToken = user.accessToken
|
|
115
|
+
rescue StandardError => e
|
|
116
|
+
UI.error("Login failed: #{e.message}")
|
|
117
|
+
raise e
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def self.ac_login_with_pak(personalAccessKey)
|
|
121
|
+
user = AuthService.get_ac_token_with_pak(personal_access_key: personalAccessKey, auth_endpoint: @@authEndpoint)
|
|
122
|
+
UI.success("Login is successful.")
|
|
123
|
+
@@apiToken = user.accessToken
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
UI.error("Login failed: #{e.message}")
|
|
126
|
+
raise e
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def self.checkTaskStatus(taskId)
|
|
130
|
+
uri = URI.parse("#{@@apiEndpoint}/task/v1/tasks/#{taskId}")
|
|
131
|
+
|
|
132
|
+
response = self.send_request(uri, @@apiToken)
|
|
133
|
+
if response.kind_of?(Net::HTTPSuccess)
|
|
134
|
+
stateValue = JSON.parse(response.body)["stateValue"]
|
|
135
|
+
if stateValue == 1
|
|
136
|
+
sleep(1)
|
|
137
|
+
return checkTaskStatus(taskId)
|
|
138
|
+
end
|
|
139
|
+
if stateValue == 3
|
|
140
|
+
return true
|
|
141
|
+
else
|
|
142
|
+
taskStatus = { 0 => "Unknown", 1 => "Begin", 2 => "Canceled", 3 => 'Completed' }
|
|
143
|
+
UI.user_error!("#{taskId} id upload request failed with status #{taskStatus[stateValue]}.")
|
|
144
|
+
end
|
|
145
|
+
else
|
|
146
|
+
UI.user_error!("Upload failed with response code #{response.code} and message '#{response.message}'.")
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Poll the publish status until it terminates (status 0=success, 1=failed,
|
|
151
|
+
# anything else = running). Logs each step's start / await / terminal once,
|
|
152
|
+
# with status icons.
|
|
153
|
+
def self.poll_publish_status(platform, profileId, appVersionId, interval: 5, max_attempts: 240)
|
|
154
|
+
step_state = {}
|
|
155
|
+
max_attempts.times do
|
|
156
|
+
data = UploadService.get_publish_object(auth_token: @@apiToken, platform: platform, publish_profile_id: profileId, app_version_id: appVersionId, api_endpoint: @@apiEndpoint)
|
|
157
|
+
(data['steps'] || []).each do |step|
|
|
158
|
+
id = step['id'] || step['name']
|
|
159
|
+
next if id.nil?
|
|
160
|
+
st = (step_state[id] ||= {})
|
|
161
|
+
status = step['status']
|
|
162
|
+
if TERMINAL_STEP_STATUSES.include?(status) && !st[:done]
|
|
163
|
+
st[:done] = true
|
|
164
|
+
UI.message("#{step_icon(status)} #{step['name']} — #{step_status_name(status)}")
|
|
165
|
+
elsif status == 203 && !st[:awaiting] && !st[:done]
|
|
166
|
+
st[:awaiting] = true
|
|
167
|
+
UI.message("#{step_icon(status)} #{step['name']} — #{step_status_name(status)}")
|
|
168
|
+
elsif ACTIVE_STEP_STATUSES.include?(status) && !st[:started] && !st[:done]
|
|
169
|
+
st[:started] = true
|
|
170
|
+
UI.message("#{step_icon(status)} #{step['name']} — #{step_status_name(status)}")
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
status = data['status'].is_a?(Numeric) ? data['status'] : 99
|
|
174
|
+
return true if status == 0
|
|
175
|
+
return false if status == 1
|
|
176
|
+
sleep(interval)
|
|
177
|
+
end
|
|
178
|
+
UI.user_error!("Publish status polling timed out.")
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def self.step_status_name(status)
|
|
182
|
+
FLOW_STEP_STATUS[status] || "Unknown (#{status})"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def self.step_icon(status)
|
|
186
|
+
case status
|
|
187
|
+
when 0 then '✅'
|
|
188
|
+
when 1 then '❌'
|
|
189
|
+
when 2 then '🚫'
|
|
190
|
+
when 3 then '⌛'
|
|
191
|
+
when 100 then '⏭️'
|
|
192
|
+
when 201 then '⏹️'
|
|
193
|
+
when 203 then '⏸️'
|
|
194
|
+
else '▶️'
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def self.send_request(uri, access_token)
|
|
199
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
200
|
+
http.use_ssl = (uri.scheme == "https")
|
|
201
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
|
202
|
+
request["Authorization"] = "Bearer #{access_token}"
|
|
203
|
+
http.request(request)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def self.description
|
|
207
|
+
"Upload a binary to an Appcircle Publish profile and/or trigger its publish flow"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def self.authors
|
|
211
|
+
["Burak Yıldırım"]
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def self.return_value
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def self.details
|
|
218
|
+
"Uploads an application binary to an existing Appcircle Publish profile and/or triggers the profile's publish flow. Upload and publish are independent options: enable either or both. When both are enabled, the uploaded version is marked as release candidate and published; a new publish is never started if one is already in progress for the profile."
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def self.available_options
|
|
222
|
+
[
|
|
223
|
+
FastlaneCore::ConfigItem.new(key: :personalAPIToken,
|
|
224
|
+
env_name: "AC_PERSONAL_API_TOKEN",
|
|
225
|
+
description: "Provide Personal API Token to authenticate Appcircle services (use either personalAPIToken or personalAccessKey)",
|
|
226
|
+
optional: true,
|
|
227
|
+
type: String),
|
|
228
|
+
|
|
229
|
+
FastlaneCore::ConfigItem.new(key: :personalAccessKey,
|
|
230
|
+
env_name: "AC_PERSONAL_ACCESS_KEY",
|
|
231
|
+
description: "Provide Personal Access Key to authenticate Appcircle services (use either personalAPIToken or personalAccessKey)",
|
|
232
|
+
optional: true,
|
|
233
|
+
type: String),
|
|
234
|
+
|
|
235
|
+
FastlaneCore::ConfigItem.new(key: :authEndpoint,
|
|
236
|
+
env_name: "AC_AUTH_ENDPOINT",
|
|
237
|
+
description: "Optional: Authentication endpoint URL for self-hosted Appcircle installations. Defaults to the Appcircle cloud",
|
|
238
|
+
optional: true,
|
|
239
|
+
default_value: "https://auth.appcircle.io",
|
|
240
|
+
type: String),
|
|
241
|
+
|
|
242
|
+
FastlaneCore::ConfigItem.new(key: :apiEndpoint,
|
|
243
|
+
env_name: "AC_API_ENDPOINT",
|
|
244
|
+
description: "Optional: API endpoint URL for self-hosted Appcircle installations. Defaults to the Appcircle cloud",
|
|
245
|
+
optional: true,
|
|
246
|
+
default_value: "https://api.appcircle.io",
|
|
247
|
+
type: String),
|
|
248
|
+
|
|
249
|
+
FastlaneCore::ConfigItem.new(key: :platform,
|
|
250
|
+
env_name: "AC_PLATFORM",
|
|
251
|
+
description: "Target platform of the Publish profile: 'ios' or 'android'",
|
|
252
|
+
optional: false,
|
|
253
|
+
type: String),
|
|
254
|
+
|
|
255
|
+
FastlaneCore::ConfigItem.new(key: :publishProfile,
|
|
256
|
+
env_name: "AC_PUBLISH_PROFILE",
|
|
257
|
+
description: "Name of the Publish profile to target. Profile names are unique per platform",
|
|
258
|
+
optional: false,
|
|
259
|
+
type: String),
|
|
260
|
+
|
|
261
|
+
FastlaneCore::ConfigItem.new(key: :upload,
|
|
262
|
+
env_name: "AC_UPLOAD",
|
|
263
|
+
description: "Upload the binary at 'appPath' as a new app version. At least one of 'upload' or 'publish' must be true",
|
|
264
|
+
optional: true,
|
|
265
|
+
default_value: false,
|
|
266
|
+
is_string: false,
|
|
267
|
+
type: Boolean),
|
|
268
|
+
|
|
269
|
+
FastlaneCore::ConfigItem.new(key: :publish,
|
|
270
|
+
env_name: "AC_PUBLISH",
|
|
271
|
+
description: "Trigger the profile's publish flow. When combined with upload, the uploaded version is marked release candidate and published; otherwise the profile's current release candidate is published",
|
|
272
|
+
optional: true,
|
|
273
|
+
default_value: false,
|
|
274
|
+
is_string: false,
|
|
275
|
+
type: Boolean),
|
|
276
|
+
|
|
277
|
+
FastlaneCore::ConfigItem.new(key: :appPath,
|
|
278
|
+
env_name: "AC_APP_PATH",
|
|
279
|
+
description: "Path to the application file. Required when 'upload' is true. For iOS use a .ipa file; for Android use a .apk or .aab file",
|
|
280
|
+
optional: true,
|
|
281
|
+
type: String)
|
|
282
|
+
]
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def self.is_supported?(platform)
|
|
286
|
+
true
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require 'fastlane_core/ui/ui'
|
|
2
|
+
|
|
3
|
+
module Fastlane
|
|
4
|
+
UI = FastlaneCore::UI unless Fastlane.const_defined?(:UI)
|
|
5
|
+
|
|
6
|
+
module Helper
|
|
7
|
+
class AppcirclePublishHelper
|
|
8
|
+
# class methods that you define here become available in your action
|
|
9
|
+
# as `Helper::AppcirclePublishHelper.your_method`
|
|
10
|
+
#
|
|
11
|
+
def self.show_message
|
|
12
|
+
UI.message("Hello from the appcircle_publish plugin helper!")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'uri'
|
|
3
|
+
require 'cgi'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
class UserResponse
|
|
7
|
+
attr_accessor :accessToken
|
|
8
|
+
|
|
9
|
+
def initialize(accessToken:)
|
|
10
|
+
@accessToken = accessToken
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module AuthService
|
|
15
|
+
def self.get_ac_token(pat:, auth_endpoint: 'https://auth.appcircle.io')
|
|
16
|
+
endpoint_url = "#{auth_endpoint}/auth/v2/token"
|
|
17
|
+
uri = URI(endpoint_url)
|
|
18
|
+
|
|
19
|
+
# Create HTTP request
|
|
20
|
+
request = Net::HTTP::Post.new(uri)
|
|
21
|
+
request.content_type = 'application/x-www-form-urlencoded'
|
|
22
|
+
request['Accept'] = 'application/json'
|
|
23
|
+
|
|
24
|
+
# Encode parameters
|
|
25
|
+
params = { pat: pat }
|
|
26
|
+
request.body = URI.encode_www_form(params)
|
|
27
|
+
|
|
28
|
+
# Make the HTTP request
|
|
29
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
30
|
+
http.request(request)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Check response
|
|
34
|
+
if response.kind_of?(Net::HTTPSuccess)
|
|
35
|
+
response_data = JSON.parse(response.body)
|
|
36
|
+
|
|
37
|
+
user = UserResponse.new(
|
|
38
|
+
accessToken: response_data['access_token']
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return user
|
|
42
|
+
else
|
|
43
|
+
raise "HTTP Request failed (#{response.code} #{response.message})"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.get_ac_token_with_pak(personal_access_key:, auth_endpoint: 'https://auth.appcircle.io')
|
|
48
|
+
endpoint_url = "#{auth_endpoint}/auth/v1/token"
|
|
49
|
+
uri = URI(endpoint_url)
|
|
50
|
+
|
|
51
|
+
# Create HTTP request
|
|
52
|
+
request = Net::HTTP::Post.new(uri)
|
|
53
|
+
request.content_type = 'application/x-www-form-urlencoded'
|
|
54
|
+
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
|
+
|
|
60
|
+
# Make the HTTP request
|
|
61
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
62
|
+
http.request(request)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Check response
|
|
66
|
+
if response.kind_of?(Net::HTTPSuccess)
|
|
67
|
+
response_data = JSON.parse(response.body)
|
|
68
|
+
|
|
69
|
+
user = UserResponse.new(
|
|
70
|
+
accessToken: response_data['access_token']
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return user
|
|
74
|
+
else
|
|
75
|
+
raise "HTTP Request failed (#{response.code} #{response.message})"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'uri'
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'rest-client'
|
|
5
|
+
|
|
6
|
+
BASE_URL = "https://api.appcircle.io"
|
|
7
|
+
|
|
8
|
+
module UploadService
|
|
9
|
+
def self.put_with_retry(url, body, headers, max_retries: 5)
|
|
10
|
+
attempt = 0
|
|
11
|
+
delay = 1.0
|
|
12
|
+
|
|
13
|
+
begin
|
|
14
|
+
RestClient.put(url, body, headers)
|
|
15
|
+
rescue => e
|
|
16
|
+
status = e.respond_to?(:http_code) ? e.http_code : nil
|
|
17
|
+
retryable = status == 503 ||
|
|
18
|
+
e.is_a?(RestClient::ServerBrokeConnection) ||
|
|
19
|
+
e.is_a?(Errno::ECONNRESET) ||
|
|
20
|
+
(defined?(RestClient::Exceptions::OpenTimeout) && e.is_a?(RestClient::Exceptions::OpenTimeout)) ||
|
|
21
|
+
(defined?(RestClient::Exceptions::ReadTimeout) && e.is_a?(RestClient::Exceptions::ReadTimeout))
|
|
22
|
+
|
|
23
|
+
raise e if !retryable || attempt >= max_retries
|
|
24
|
+
|
|
25
|
+
attempt += 1
|
|
26
|
+
sleep(delay + rand(0.3)) # backoff + jitter (0-300ms)
|
|
27
|
+
delay *= 2
|
|
28
|
+
retry
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Resolve a publish profile id from its name for the given platform.
|
|
33
|
+
# Profile names are unique per (organization, platform), so the match is exact.
|
|
34
|
+
def self.get_publish_profile_id(auth_token:, platform:, profile_name:, api_endpoint: BASE_URL)
|
|
35
|
+
url = "#{api_endpoint}/publish/v2/profiles/#{platform}"
|
|
36
|
+
headers = { Authorization: "Bearer #{auth_token}", accept: 'application/json' }
|
|
37
|
+
|
|
38
|
+
profiles = JSON.parse(RestClient.get(url, headers).body)
|
|
39
|
+
profile = (profiles || []).find { |p| p['name'] == profile_name }
|
|
40
|
+
if profile.nil?
|
|
41
|
+
UI.user_error!("Publish profile '#{profile_name}' not found for platform '#{platform}'.")
|
|
42
|
+
end
|
|
43
|
+
profile['id']
|
|
44
|
+
rescue RestClient::ExceptionWithResponse => e
|
|
45
|
+
raise e
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.upload_artifact(token:, app:, platform:, publish_profile_id:, api_endpoint: BASE_URL)
|
|
49
|
+
file_path = app
|
|
50
|
+
file_name = File.basename(file_path)
|
|
51
|
+
file_size = File.size(file_path)
|
|
52
|
+
auth_header = { Authorization: "Bearer #{token}", accept: 'application/json' }
|
|
53
|
+
# Profile listing is v2, but the signed-URL upload/commit actions live on v1.
|
|
54
|
+
base = "#{api_endpoint}/publish/v1/profiles/#{platform}/#{publish_profile_id}/app-versions"
|
|
55
|
+
|
|
56
|
+
begin
|
|
57
|
+
info_uri = URI(base)
|
|
58
|
+
info_uri.query = URI.encode_www_form({ action: 'uploadInformation', fileName: file_name, fileSize: file_size })
|
|
59
|
+
upload_info = JSON.parse(RestClient.get(info_uri.to_s, auth_header).body)
|
|
60
|
+
file_id = upload_info['fileId']
|
|
61
|
+
upload_url = upload_info['uploadUrl']
|
|
62
|
+
configuration = upload_info['configuration']
|
|
63
|
+
http_method = (configuration && configuration['httpMethod']) ? configuration['httpMethod'].to_s.upcase : 'PUT'
|
|
64
|
+
|
|
65
|
+
if http_method == 'POST'
|
|
66
|
+
sign_parameters = configuration['signParameters'] || {}
|
|
67
|
+
payload = {}
|
|
68
|
+
sign_parameters.each { |key, value| payload[key] = value }
|
|
69
|
+
payload['file'] = File.new(file_path, 'rb') # the 'file' field MUST be last
|
|
70
|
+
RestClient.post(upload_url, payload)
|
|
71
|
+
else
|
|
72
|
+
put_with_retry(upload_url, File.binread(file_path), { content_type: 'application/octet-stream' })
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
commit_uri = URI(base)
|
|
76
|
+
commit_uri.query = URI.encode_www_form({ action: 'commitFileUpload' })
|
|
77
|
+
|
|
78
|
+
commit_payload = { fileId: file_id, fileName: file_name }.to_json
|
|
79
|
+
commit_headers = { Authorization: "Bearer #{token}", content_type: :json, accept: 'application/json' }
|
|
80
|
+
JSON.parse(RestClient.post(commit_uri.to_s, commit_payload, commit_headers).body)
|
|
81
|
+
rescue RestClient::ExceptionWithResponse => e
|
|
82
|
+
raise e
|
|
83
|
+
rescue StandardError => e
|
|
84
|
+
raise e
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def self.auth_headers(token)
|
|
89
|
+
{ Authorization: "Bearer #{token}", accept: 'application/json' }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.get_app_versions(auth_token:, platform:, publish_profile_id:, api_endpoint: BASE_URL)
|
|
93
|
+
url = "#{api_endpoint}/publish/v2/profiles/#{platform}/#{publish_profile_id}/app-versions"
|
|
94
|
+
data = JSON.parse(RestClient.get(url, auth_headers(auth_token)).body)
|
|
95
|
+
data.is_a?(Array) ? data : (data['data'] || [])
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The most recently created app version (used after an upload).
|
|
99
|
+
def self.get_latest_app_version_id(auth_token:, platform:, publish_profile_id:, api_endpoint: BASE_URL)
|
|
100
|
+
versions = get_app_versions(auth_token: auth_token, platform: platform, publish_profile_id: publish_profile_id, api_endpoint: api_endpoint)
|
|
101
|
+
UI.user_error!("No app versions found on the publish profile after upload.") if versions.empty?
|
|
102
|
+
versions[0]['id']
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The profile's current release candidate (published in publish-only mode).
|
|
106
|
+
def self.get_release_candidate_version_id(auth_token:, platform:, publish_profile_id:, api_endpoint: BASE_URL)
|
|
107
|
+
versions = get_app_versions(auth_token: auth_token, platform: platform, publish_profile_id: publish_profile_id, api_endpoint: api_endpoint)
|
|
108
|
+
rc = versions.find { |v| v['releaseCandidate'] == true }
|
|
109
|
+
UI.user_error!("No release candidate app version found on the publish profile. Mark a version as release candidate (or enable upload) before publishing.") if rc.nil?
|
|
110
|
+
rc['id']
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def self.mark_release_candidate(auth_token:, platform:, publish_profile_id:, app_version_id:, api_endpoint: BASE_URL)
|
|
114
|
+
uri = URI("#{api_endpoint}/publish/v2/profiles/#{platform}/#{publish_profile_id}/app-versions/#{app_version_id}")
|
|
115
|
+
uri.query = URI.encode_www_form({ action: 'releaseCandidate' })
|
|
116
|
+
headers = { Authorization: "Bearer #{auth_token}", content_type: :json, accept: 'application/json' }
|
|
117
|
+
RestClient.patch(uri.to_s, { ReleaseCandidate: true }.to_json, headers)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# In-progress publishes for the given profile (scope: target profile).
|
|
121
|
+
def self.get_active_publish_count_for_profile(auth_token:, publish_profile_id:, api_endpoint: BASE_URL)
|
|
122
|
+
url = "#{api_endpoint}/build/v1/queue/my-dashboard?page=1&size=1000"
|
|
123
|
+
data = JSON.parse(RestClient.get(url, auth_headers(auth_token)).body)
|
|
124
|
+
items = data['data'] || []
|
|
125
|
+
items.count { |p| p['publishId'] && p['profileId'] == publish_profile_id }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.get_publish_id(auth_token:, platform:, publish_profile_id:, app_version_id:, api_endpoint: BASE_URL)
|
|
129
|
+
url = "#{api_endpoint}/publish/v2/profiles/#{platform}/#{publish_profile_id}/app-versions/#{app_version_id}/publish"
|
|
130
|
+
data = JSON.parse(RestClient.get(url, auth_headers(auth_token)).body)
|
|
131
|
+
steps = data['steps'] || []
|
|
132
|
+
publish_id = steps[0] && steps[0]['publishId']
|
|
133
|
+
UI.user_error!("No publish flow steps found for the app version. Configure a publish flow on the profile first.") if publish_id.nil?
|
|
134
|
+
publish_id
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def self.start_publish(auth_token:, platform:, publish_profile_id:, publish_id:, api_endpoint: BASE_URL)
|
|
138
|
+
uri = URI("#{api_endpoint}/publish/v2/profiles/#{platform}/#{publish_profile_id}/publish/#{publish_id}")
|
|
139
|
+
uri.query = URI.encode_www_form({ action: 'restart' })
|
|
140
|
+
headers = { Authorization: "Bearer #{auth_token}", content_type: :json, accept: 'application/json' }
|
|
141
|
+
RestClient.post(uri.to_s, "{}", headers)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Single fetch of the publish object (status + steps). Polled by the action.
|
|
145
|
+
def self.get_publish_object(auth_token:, platform:, publish_profile_id:, app_version_id:, api_endpoint: BASE_URL)
|
|
146
|
+
url = "#{api_endpoint}/publish/v1/profiles/#{platform}/#{publish_profile_id}/app-versions/#{app_version_id}/publish"
|
|
147
|
+
JSON.parse(RestClient.get(url, auth_headers(auth_token)).body)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require 'fastlane/plugin/appcircle_publish/version'
|
|
2
|
+
|
|
3
|
+
module Fastlane
|
|
4
|
+
module AppcirclePublish
|
|
5
|
+
# Return all .rb files inside the "actions" and "helper" directory
|
|
6
|
+
def self.all_classes
|
|
7
|
+
Dir[File.expand_path('**/{actions,helper}/*.rb', File.dirname(__FILE__))]
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# By default we want to import all available actions and helpers
|
|
13
|
+
# A plugin can contain any number of actions and plugins
|
|
14
|
+
Fastlane::AppcirclePublish.all_classes.each do |current|
|
|
15
|
+
require current
|
|
16
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fastlane-plugin-appcircle_publish
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0.beta.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- appcircleio
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-06-30 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rest-client
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
description:
|
|
28
|
+
email: cloud@appcircle.io
|
|
29
|
+
executables: []
|
|
30
|
+
extensions: []
|
|
31
|
+
extra_rdoc_files: []
|
|
32
|
+
files:
|
|
33
|
+
- LICENSE
|
|
34
|
+
- README.md
|
|
35
|
+
- lib/fastlane/plugin/appcircle_publish.rb
|
|
36
|
+
- lib/fastlane/plugin/appcircle_publish/actions/appcircle_publish_action.rb
|
|
37
|
+
- lib/fastlane/plugin/appcircle_publish/helper/appcircle_publish_helper.rb
|
|
38
|
+
- lib/fastlane/plugin/appcircle_publish/helper/auth_service.rb
|
|
39
|
+
- lib/fastlane/plugin/appcircle_publish/helper/upload_service.rb
|
|
40
|
+
- lib/fastlane/plugin/appcircle_publish/version.rb
|
|
41
|
+
homepage: https://github.com/appcircleio/fastlane-plugin-appcircle_publish
|
|
42
|
+
licenses:
|
|
43
|
+
- MIT
|
|
44
|
+
metadata:
|
|
45
|
+
rubygems_mfa_required: 'true'
|
|
46
|
+
post_install_message:
|
|
47
|
+
rdoc_options: []
|
|
48
|
+
require_paths:
|
|
49
|
+
- lib
|
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '2.6'
|
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: 1.3.1
|
|
60
|
+
requirements: []
|
|
61
|
+
rubygems_version: 3.3.27
|
|
62
|
+
signing_key:
|
|
63
|
+
specification_version: 4
|
|
64
|
+
summary: Upload an application binary to an Appcircle Publish profile
|
|
65
|
+
test_files: []
|