Class: Fastlane::Actions::AppcirclePublishAction
- Inherits:
-
Action
- Object
- Action
- Fastlane::Actions::AppcirclePublishAction
- Defined in:
- lib/fastlane/plugin/appcircle_publish/actions/appcircle_publish_action.rb
Constant Summary collapse
- FLOW_STEP_STATUS =
{ 0 => 'Success', 1 => 'Failed', 2 => 'Cancelled', 3 => 'Timeout', 90 => 'Waiting', 91 => 'Running', 92 => 'Completing', 99 => 'Unknown', 100 => 'Skipped', 200 => 'Not Started', 201 => 'Stopped', 202 => 'In Progress', 203 => 'Awaiting Response' }.freeze
- TERMINAL_STEP_STATUSES =
[0, 1, 2, 3, 100, 201].freeze
- ACTIVE_STEP_STATUSES =
[91, 92, 202].freeze
- @@apiToken =
nil- @@authEndpoint =
"https://auth.appcircle.io"- @@apiEndpoint =
"https://api.appcircle.io"
Class Method Summary collapse
- .ac_login_with_pak(personalAccessKey) ⇒ Object
- .ac_login_with_pat(accessToken) ⇒ Object
- .authors ⇒ Object
- .available_options ⇒ Object
- .checkTaskStatus(taskId) ⇒ Object
- .description ⇒ Object
- .details ⇒ Object
- .is_supported?(platform) ⇒ Boolean
-
.poll_publish_status(platform, profileId, appVersionId, interval: 5, max_attempts: 240) ⇒ Object
Poll the publish status until it terminates (status 0=success, 1=failed, anything else = running).
- .return_value ⇒ Object
- .run(params) ⇒ Object
- .send_request(uri, access_token) ⇒ Object
- .step_icon(status) ⇒ Object
- .step_status_name(status) ⇒ Object
Class Method Details
.ac_login_with_pak(personalAccessKey) ⇒ Object
120 121 122 123 124 125 126 127 |
# File 'lib/fastlane/plugin/appcircle_publish/actions/appcircle_publish_action.rb', line 120 def self.ac_login_with_pak(personalAccessKey) user = AuthService.get_ac_token_with_pak(personal_access_key: personalAccessKey, auth_endpoint: @@authEndpoint) UI.success("Login is successful.") @@apiToken = user.accessToken rescue StandardError => e UI.error("Login failed: #{e.}") raise e end |
.ac_login_with_pat(accessToken) ⇒ Object
111 112 113 114 115 116 117 118 |
# File 'lib/fastlane/plugin/appcircle_publish/actions/appcircle_publish_action.rb', line 111 def self.ac_login_with_pat(accessToken) user = AuthService.get_ac_token(pat: accessToken, auth_endpoint: @@authEndpoint) UI.success("Login is successful.") @@apiToken = user.accessToken rescue StandardError => e UI.error("Login failed: #{e.}") raise e end |
.authors ⇒ Object
210 211 212 |
# File 'lib/fastlane/plugin/appcircle_publish/actions/appcircle_publish_action.rb', line 210 def self. ["Burak Yıldırım"] end |
.available_options ⇒ Object
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 |
# File 'lib/fastlane/plugin/appcircle_publish/actions/appcircle_publish_action.rb', line 221 def self. [ FastlaneCore::ConfigItem.new(key: :personalAPIToken, env_name: "AC_PERSONAL_API_TOKEN", description: "Provide Personal API Token to authenticate Appcircle services (use either personalAPIToken or personalAccessKey)", optional: true, type: String), FastlaneCore::ConfigItem.new(key: :personalAccessKey, env_name: "AC_PERSONAL_ACCESS_KEY", description: "Provide Personal Access Key to authenticate Appcircle services (use either personalAPIToken or personalAccessKey)", optional: true, type: String), FastlaneCore::ConfigItem.new(key: :authEndpoint, env_name: "AC_AUTH_ENDPOINT", description: "Optional: Authentication endpoint URL for self-hosted Appcircle installations. Defaults to the Appcircle cloud", optional: true, default_value: "https://auth.appcircle.io", type: String), FastlaneCore::ConfigItem.new(key: :apiEndpoint, env_name: "AC_API_ENDPOINT", description: "Optional: API endpoint URL for self-hosted Appcircle installations. Defaults to the Appcircle cloud", optional: true, default_value: "https://api.appcircle.io", type: String), FastlaneCore::ConfigItem.new(key: :platform, env_name: "AC_PLATFORM", description: "Target platform of the Publish profile: 'ios' or 'android'", optional: false, type: String), FastlaneCore::ConfigItem.new(key: :publishProfile, env_name: "AC_PUBLISH_PROFILE", description: "Name of the Publish profile to target. Profile names are unique per platform", optional: false, type: String), FastlaneCore::ConfigItem.new(key: :upload, env_name: "AC_UPLOAD", description: "Upload the binary at 'appPath' as a new app version. At least one of 'upload' or 'publish' must be true", optional: true, default_value: false, is_string: false, type: Boolean), FastlaneCore::ConfigItem.new(key: :publish, env_name: "AC_PUBLISH", 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", optional: true, default_value: false, is_string: false, type: Boolean), FastlaneCore::ConfigItem.new(key: :appPath, env_name: "AC_APP_PATH", 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", optional: true, type: String) ] end |
.checkTaskStatus(taskId) ⇒ Object
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/fastlane/plugin/appcircle_publish/actions/appcircle_publish_action.rb', line 129 def self.checkTaskStatus(taskId) uri = URI.parse("#{@@apiEndpoint}/task/v1/tasks/#{taskId}") response = self.send_request(uri, @@apiToken) if response.kind_of?(Net::HTTPSuccess) stateValue = JSON.parse(response.body)["stateValue"] if stateValue == 1 sleep(1) return checkTaskStatus(taskId) end if stateValue == 3 return true else taskStatus = { 0 => "Unknown", 1 => "Begin", 2 => "Canceled", 3 => 'Completed' } UI.user_error!("#{taskId} id upload request failed with status #{taskStatus[stateValue]}.") end else UI.user_error!("Upload failed with response code #{response.code} and message '#{response.}'.") end end |
.description ⇒ Object
206 207 208 |
# File 'lib/fastlane/plugin/appcircle_publish/actions/appcircle_publish_action.rb', line 206 def self.description "Upload a binary to an Appcircle Publish profile and/or trigger its publish flow" end |
.details ⇒ Object
217 218 219 |
# File 'lib/fastlane/plugin/appcircle_publish/actions/appcircle_publish_action.rb', line 217 def self.details "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." end |
.is_supported?(platform) ⇒ Boolean
285 286 287 |
# File 'lib/fastlane/plugin/appcircle_publish/actions/appcircle_publish_action.rb', line 285 def self.is_supported?(platform) true end |
.poll_publish_status(platform, profileId, appVersionId, interval: 5, max_attempts: 240) ⇒ Object
Poll the publish status until it terminates (status 0=success, 1=failed, anything else = running). Logs each step's start / await / terminal once, with status icons.
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/fastlane/plugin/appcircle_publish/actions/appcircle_publish_action.rb', line 153 def self.poll_publish_status(platform, profileId, appVersionId, interval: 5, max_attempts: 240) step_state = {} max_attempts.times do data = UploadService.get_publish_object(auth_token: @@apiToken, platform: platform, publish_profile_id: profileId, app_version_id: appVersionId, api_endpoint: @@apiEndpoint) (data['steps'] || []).each do |step| id = step['id'] || step['name'] next if id.nil? st = (step_state[id] ||= {}) status = step['status'] if TERMINAL_STEP_STATUSES.include?(status) && !st[:done] st[:done] = true UI.("#{step_icon(status)} #{step['name']} — #{step_status_name(status)}") elsif status == 203 && !st[:awaiting] && !st[:done] st[:awaiting] = true UI.("#{step_icon(status)} #{step['name']} — #{step_status_name(status)}") elsif ACTIVE_STEP_STATUSES.include?(status) && !st[:started] && !st[:done] st[:started] = true UI.("#{step_icon(status)} #{step['name']} — #{step_status_name(status)}") end end status = data['status'].is_a?(Numeric) ? data['status'] : 99 return true if status == 0 return false if status == 1 sleep(interval) end UI.user_error!("Publish status polling timed out.") end |
.return_value ⇒ Object
214 215 |
# File 'lib/fastlane/plugin/appcircle_publish/actions/appcircle_publish_action.rb', line 214 def self.return_value end |
.run(params) ⇒ Object
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
# File 'lib/fastlane/plugin/appcircle_publish/actions/appcircle_publish_action.rb', line 28 def self.run(params) personalAPIToken = params[:personalAPIToken] personalAccessKey = params[:personalAccessKey] @@authEndpoint = params[:authEndpoint] @@apiEndpoint = params[:apiEndpoint] platform = params[:platform] && params[:platform].downcase publishProfile = params[:publishProfile] appPath = params[:appPath] upload = params[:upload] publish = params[:publish] # --- Validation --- if !upload && !publish UI.user_error!("Nothing to do: set 'upload' and/or 'publish' to true.") end if personalAPIToken.nil? && personalAccessKey.nil? UI.user_error!("Please provide either Personal API Token (personalAPIToken) or Personal Access Key (personalAccessKey) to authenticate connections to Appcircle services") elsif !personalAPIToken.nil? && !personalAccessKey.nil? UI.user_error!("Please provide only one authentication method: either Personal API Token (personalAPIToken) or Personal Access Key (personalAccessKey), not both") end if platform.nil? || (platform != "ios" && platform != "android") UI.user_error!("Please provide a valid platform for the Publish profile: 'ios' or 'android'") end if publishProfile.nil? UI.user_error!("Please provide the name of the Publish profile to target") end if upload 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? valid_extensions = ['.apk', '.aab', '.ipa'] file_extension = File.extname(appPath).downcase unless valid_extensions.include?(file_extension) raise "Invalid file extension: #{file_extension}. For Android, use .apk or .aab. For iOS, use .ipa." end end # --- Auth + profile --- if personalAPIToken.nil? self.ac_login_with_pak(personalAccessKey) else self.ac_login_with_pat(personalAPIToken) end profileId = UploadService.get_publish_profile_id(auth_token: @@apiToken, platform: platform, profile_name: publishProfile, api_endpoint: @@apiEndpoint) # Guard: never start a new publish if one is already running for the profile. if publish active = UploadService.get_active_publish_count_for_profile(auth_token: @@apiToken, publish_profile_id: profileId, api_endpoint: @@apiEndpoint) if active > 0 UI.user_error!("A publish is already in progress for profile '#{publishProfile}'. Not starting a new one.") end end appVersionId = nil # --- Upload --- if upload response = UploadService.upload_artifact(token: @@apiToken, app: appPath, platform: platform, publish_profile_id: profileId, api_endpoint: @@apiEndpoint) self.checkTaskStatus(response["taskId"]) appVersionId = UploadService.get_latest_app_version_id(auth_token: @@apiToken, platform: platform, publish_profile_id: profileId, api_endpoint: @@apiEndpoint) UI.success("#{appPath} uploaded to the Appcircle Publish profile '#{publishProfile}' successfully") end # --- Publish --- if publish if upload && appVersionId UploadService.mark_release_candidate(auth_token: @@apiToken, platform: platform, publish_profile_id: profileId, app_version_id: appVersionId, api_endpoint: @@apiEndpoint) UI.("Marked the uploaded version as release candidate.") else appVersionId = UploadService.get_release_candidate_version_id(auth_token: @@apiToken, platform: platform, publish_profile_id: profileId, api_endpoint: @@apiEndpoint) end publishId = UploadService.get_publish_id(auth_token: @@apiToken, platform: platform, publish_profile_id: profileId, app_version_id: appVersionId, api_endpoint: @@apiEndpoint) UploadService.start_publish(auth_token: @@apiToken, platform: platform, publish_profile_id: profileId, publish_id: publishId, api_endpoint: @@apiEndpoint) UI.success("Publish flow started for profile '#{publishProfile}'.") success = self.poll_publish_status(platform, profileId, appVersionId) UI.user_error!("Publish flow failed.") unless success end end |
.send_request(uri, access_token) ⇒ Object
198 199 200 201 202 203 204 |
# File 'lib/fastlane/plugin/appcircle_publish/actions/appcircle_publish_action.rb', line 198 def self.send_request(uri, access_token) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = (uri.scheme == "https") request = Net::HTTP::Get.new(uri.request_uri) request["Authorization"] = "Bearer #{access_token}" http.request(request) end |
.step_icon(status) ⇒ Object
185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/fastlane/plugin/appcircle_publish/actions/appcircle_publish_action.rb', line 185 def self.step_icon(status) case status when 0 then '✅' when 1 then '❌' when 2 then '🚫' when 3 then '⌛' when 100 then '⏭️' when 201 then '⏹️' when 203 then '⏸️' else '▶️' end end |
.step_status_name(status) ⇒ Object
181 182 183 |
# File 'lib/fastlane/plugin/appcircle_publish/actions/appcircle_publish_action.rb', line 181 def self.step_status_name(status) FLOW_STEP_STATUS[status] || "Unknown (#{status})" end |