Class: Clacky::DeployApiClient
- Inherits:
-
Object
- Object
- Clacky::DeployApiClient
- Defined in:
- lib/clacky/deploy_api_client.rb
Overview
DeployApiClient - Encapsulates all Deploy API calls for the Railway deployment flow.
All endpoints use Workspace API Key (clacky_ak_*) authentication, as the backend supports both clacky_ak_* and clacky_dk_* for all deploy endpoints.
Usage:
client = DeployApiClient.new("clacky_ak_xxx", base_url: "https://api.clacky.ai")
# Check payment status
result = client.payment_status(project_id: "proj_abc")
# => { success: true, is_paid: true }
# Create a deploy task
result = client.create_task(project_id: "proj_abc")
# => { success: true, deploy_task_id: "...", platform_token: "...", ... }
# Poll services until DB is ready
result = client.services(deploy_task_id: "task_abc")
# => { success: true, services: [...], domain_name: "..." }
# Poll deploy status
result = client.deploy_status(deploy_task_id: "task_abc")
# => { success: true, status: "SUCCESS", url: "https://..." }
# Bind domain
result = client.bind_domain(deploy_task_id: "task_abc")
# => { success: true, domain: "my-app.example.com" }
# Notify backend of deploy outcome
client.notify(project_id: "...", deploy_task_id: "...", status: "success")
Constant Summary collapse
- BASE_PATH =
"/openclacky/v1"- REQUEST_TIMEOUT =
seconds for normal requests
30- OPEN_TIMEOUT =
seconds for connection
10
Instance Method Summary collapse
-
#bind_domain(deploy_task_id:) ⇒ Hash
Bind a custom domain to the deploy task.
-
#build_logs(deploy_task_id:, service_id: nil, level: "INFO", lines: 100) ⇒ Hash
Fetch build logs for a deploy task (synchronous, not SSE).
-
#create_task(project_id:, backup_db: false, env_vars: {}, region: nil) ⇒ Hash
Create a new deployment task on the backend.
-
#deploy_status(deploy_task_id:) ⇒ Hash
Query the real-time deployment status for a task.
-
#initialize(workspace_key, base_url:) ⇒ DeployApiClient
constructor
A new instance of DeployApiClient.
-
#notify(project_id:, deploy_task_id:, status:, deploy_service_id: nil, message: nil, target_port: nil) ⇒ Hash
Notify the backend of the current deployment outcome.
-
#payment_status(project_id:) ⇒ Hash
Query whether the project has an active paid subscription.
-
#regions(project_id:) ⇒ Hash
Fetch the list of supported deployment regions.
-
#services(deploy_task_id:) ⇒ Hash
Query all services under a deploy task.
-
#stream_build_logs(deploy_task_id:, service_id: nil, level: "INFO") {|Hash| ... } ⇒ Hash
Stream build logs using SSE (Server-Sent Events).
Constructor Details
#initialize(workspace_key, base_url:) ⇒ DeployApiClient
Returns a new instance of DeployApiClient.
42 43 44 45 |
# File 'lib/clacky/deploy_api_client.rb', line 42 def initialize(workspace_key, base_url:) @workspace_key = workspace_key.to_s.strip @base_url = base_url.to_s.strip.sub(%r{/+$}, "") end |
Instance Method Details
#bind_domain(deploy_task_id:) ⇒ Hash
Bind a custom domain to the deploy task.
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 |
# File 'lib/clacky/deploy_api_client.rb', line 280 def bind_domain(deploy_task_id:) response = connection.post("#{BASE_PATH}/deploy/bind-domain") do |req| req.headers["Content-Type"] = "application/json" req.body = JSON.generate({ deploy_task_id: deploy_task_id }) end return http_error(response) unless response.status == 200 body = parse_body(response) return body_error(body) unless success_code?(body) data = body["data"] || {} { success: true, domain: data["domain"].to_s } rescue Faraday::Error => e { success: false, error: "Network error: #{e.}" } rescue => e { success: false, error: "Unexpected error: #{e.}" } end |
#build_logs(deploy_task_id:, service_id: nil, level: "INFO", lines: 100) ⇒ Hash
Fetch build logs for a deploy task (synchronous, not SSE).
} or { success: false, error: String }
313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 |
# File 'lib/clacky/deploy_api_client.rb', line 313 def build_logs(deploy_task_id:, service_id: nil, level: "INFO", lines: 100) body_params = { deploy_task_id: deploy_task_id, level: level, lines: lines } body_params[:service_id] = service_id if service_id response = connection.post("#{BASE_PATH}/tasks/logs") do |req| req.headers["Content-Type"] = "application/json" req.body = JSON.generate(body_params) end return http_error(response) unless response.status == 200 body = parse_body(response) return body_error(body) unless success_code?(body) data = body["data"] || {} logs = data["logs"] || [] { success: true, logs: logs } rescue Faraday::Error => e { success: false, error: "Network error: #{e.}" } rescue => e { success: false, error: "Unexpected error: #{e.}" } end |
#create_task(project_id:, backup_db: false, env_vars: {}, region: nil) ⇒ Hash
Create a new deployment task on the backend. Returns Railway credentials.
}
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/clacky/deploy_api_client.rb', line 122 def create_task(project_id:, backup_db: false, env_vars: {}, region: nil) body_params = { project_id: project_id, backup_db: backup_db } body_params[:env_vars] = env_vars unless env_vars.empty? body_params[:region] = region if region response = connection.post("#{BASE_PATH}/deploy/create-task") do |req| req.headers["Content-Type"] = "application/json" req.body = JSON.generate(body_params) end return http_error(response) unless response.status == 200 body = parse_body(response) return body_error(body) unless success_code?(body) data = body["data"] || {} { success: true, deploy_task_id: data["deploy_task_id"], deploy_service_id: data["deploy_service_id"], platform_token: data["platform_token"], platform_project_id: data["platform_project_id"], platform_environment_id: data["platform_environment_id"] } rescue Faraday::Error => e { success: false, error: "Network error: #{e.}" } rescue => e { success: false, error: "Unexpected error: #{e.}" } end |
#deploy_status(deploy_task_id:) ⇒ Hash
Query the real-time deployment status for a task.
}
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 |
# File 'lib/clacky/deploy_api_client.rb', line 249 def deploy_status(deploy_task_id:) response = connection.get("#{BASE_PATH}/deploy/status") do |req| req.params["deploy_task_id"] = deploy_task_id end return http_error(response) unless response.status == 200 body = parse_body(response) return body_error(body) unless success_code?(body) data = body["data"] || {} { success: true, status: data["status"].to_s.upcase, url: data["url"].to_s, deploy_service_id: data["deploy_service_id"].to_s } rescue Faraday::Error => e { success: false, error: "Network error: #{e.}" } rescue => e { success: false, error: "Unexpected error: #{e.}" } end |
#notify(project_id:, deploy_task_id:, status:, deploy_service_id: nil, message: nil, target_port: nil) ⇒ Hash
Notify the backend of the current deployment outcome. Fire-and-forget — failures are logged but do not raise.
409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 |
# File 'lib/clacky/deploy_api_client.rb', line 409 def notify(project_id:, deploy_task_id:, status:, deploy_service_id: nil, message: nil, target_port: nil) payload = { project_id: project_id, deploy_task_id: deploy_task_id, status: status } payload[:deploy_service_id] = deploy_service_id if deploy_service_id payload[:message] = if payload[:target_port] = target_port if target_port response = connection.post("#{BASE_PATH}/deploy/notify") do |req| req.headers["Content-Type"] = "application/json" req.body = JSON.generate(payload) end return http_error(response) unless response.status == 200 body = parse_body(response) return body_error(body) unless success_code?(body) { success: true } rescue => e # Notify failures are non-fatal — log and move on warn "[deploy_api] notify failed: #{e.}" { success: false, error: e. } end |
#payment_status(project_id:) ⇒ Hash
Query whether the project has an active paid subscription.
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/clacky/deploy_api_client.rb', line 55 def payment_status(project_id:) response = connection.get("#{BASE_PATH}/deploy/payment") do |req| req.params["project_id"] = project_id end return http_error(response) unless response.status == 200 body = parse_body(response) return body_error(body) unless success_code?(body) data = body["data"] || {} { success: true, is_paid: data["is_paid"] == true } rescue Faraday::Error => e { success: false, error: "Network error: #{e.}" } rescue => e { success: false, error: "Unexpected error: #{e.}" } end |
#regions(project_id:) ⇒ Hash
Fetch the list of supported deployment regions.
} or { success: false, error: String }
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/clacky/deploy_api_client.rb', line 84 def regions(project_id:) response = connection.get("#{BASE_PATH}/deploy/regions") do |req| req.params["project_id"] = project_id end return http_error(response) unless response.status == 200 body = parse_body(response) return body_error(body) unless success_code?(body) data = body["data"] || {} list = data["regions"] || data || [] list = list.values if list.is_a?(Hash) { success: true, regions: Array(list) } rescue Faraday::Error => e { success: false, error: "Network error: #{e.}" } rescue => e { success: false, error: "Unexpected error: #{e.}" } end |
#services(deploy_task_id:) ⇒ Hash
Query all services under a deploy task. Used to wait for the PostgreSQL middleware to reach status SUCCESS before injecting the DATABASE_URL reference.
}
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 |
# File 'lib/clacky/deploy_api_client.rb', line 167 def services(deploy_task_id:) url = "#{BASE_PATH}/deploy/services?deploy_task_id=#{deploy_task_id}" puts " [DEBUG API] GET #{@base_url}#{url}" response = connection.get("#{BASE_PATH}/deploy/services") do |req| req.params["deploy_task_id"] = deploy_task_id end puts " [DEBUG API] Response status: #{response.status}" return http_error(response) unless response.status == 200 body = parse_body(response) puts " [DEBUG API] Response body: #{body.inspect[0..500]}..." if body return body_error(body) unless success_code?(body) data = body["data"] || {} svcs = data["services"] || [] domain = data["domain_name"].to_s # Debug: print detailed service info puts " [DEBUG] Total services returned: #{svcs.size}" svcs.each_with_index do |s, idx| puts " [DEBUG] Service[#{idx}]: name=#{s['service_name']}, type=#{s['type']}, status=#{s['status']}" if s["type"] == "middleware" env_vars = s["env_vars"] || {} puts " [DEBUG] - env_vars keys: #{env_vars.keys.join(', ')}" puts " [DEBUG] - has DATABASE_URL: #{env_vars.key?('DATABASE_URL')}" puts " [DEBUG] - has DATABASE_PUBLIC_URL: #{env_vars.key?('DATABASE_PUBLIC_URL')}" end end # Find first middleware (DB) that is fully provisioned db_svc = svcs.find do |s| s["type"] == "middleware" && s["status"]&.upcase == "SUCCESS" end puts " [DEBUG] db_svc found: #{!db_svc.nil?}" if db_svc puts " [DEBUG] - db_svc name: #{db_svc['service_name']}" puts " [DEBUG] - db_svc status: #{db_svc['status']}" end # middleware_support: { supported: Boolean, supported_types: Array } # When supported == false, no DB middleware will be provisioned by Clacky. # The deploy script uses this to skip the DB polling loop entirely. middleware_support = data["middleware_support"] || {} puts " [DEBUG] middleware_support: #{middleware_support.inspect}" # platform_bucket_credentials contains S3-compatible storage credentials. # Passed through so the deploy script can inject STORAGE_BUCKET_* env vars. bucket_credentials = data["platform_bucket_credentials"] bucket_name = data["platform_bucket_name"].to_s { success: true, services: svcs, domain_name: domain, db_service: db_svc, middleware_support: middleware_support, bucket_credentials: bucket_credentials, bucket_name: bucket_name } rescue Faraday::Error => e { success: false, error: "Network error: #{e.}" } rescue => e { success: false, error: "Unexpected error: #{e.}" } end |
#stream_build_logs(deploy_task_id:, service_id: nil, level: "INFO") {|Hash| ... } ⇒ Hash
Stream build logs using SSE (Server-Sent Events). This method yields each log line as it arrives.
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 |
# File 'lib/clacky/deploy_api_client.rb', line 348 def stream_build_logs(deploy_task_id:, service_id: nil, level: "INFO", &block) require "net/http" require "openssl" body_params = { deploy_task_id: deploy_task_id, level: level } body_params[:service_id] = service_id if service_id url = "#{@base_url}#{BASE_PATH}/tasks/stream/build-logs" uri = URI.parse(url) Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http| request = Net::HTTP::Post.new(uri.path) request["Authorization"] = "Bearer #{@workspace_key}" request["Accept"] = "text/event-stream" request["Content-Type"] = "application/json" request.body = JSON.generate(body_params) http.request(request) do |response| return { success: false, error: "HTTP #{response.code}: #{response.}" } unless response.code.to_i == 200 buffer = "" response.read_body do |chunk| buffer << chunk while (line_end = buffer.index("\n")) line = buffer.slice!(0..line_end).strip next if line.empty? || !line.start_with?("data:") json_str = line.sub(/^data:\s*/, "") begin event = JSON.parse(json_str) block.call(event) if block rescue JSON::ParserError # Ignore malformed JSON end end end end end { success: true } rescue => e { success: false, error: "Stream error: #{e.}" } end |