openclacky 1.0.0 → 1.0.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/CHANGELOG.md +39 -0
- data/README.md +87 -53
- data/lib/clacky/agent/cost_tracker.rb +19 -2
- data/lib/clacky/agent/llm_caller.rb +218 -0
- data/lib/clacky/agent/message_compressor_helper.rb +32 -2
- data/lib/clacky/agent.rb +54 -22
- data/lib/clacky/client.rb +44 -5
- data/lib/clacky/default_parsers/pdf_parser.rb +58 -17
- data/lib/clacky/default_parsers/pdf_parser_ocr.py +103 -0
- data/lib/clacky/default_parsers/pdf_parser_plumber.py +62 -0
- data/lib/clacky/default_skills/deploy/SKILL.md +201 -77
- data/lib/clacky/default_skills/new/SKILL.md +3 -114
- data/lib/clacky/default_skills/onboard/SKILL.md +349 -133
- data/lib/clacky/default_skills/onboard/scripts/import_external_skills.rb +371 -0
- data/lib/clacky/default_skills/onboard/scripts/install_builtin_skills.rb +175 -0
- data/lib/clacky/default_skills/skill-add/scripts/install_from_zip.rb +59 -26
- data/lib/clacky/message_format/anthropic.rb +72 -8
- data/lib/clacky/message_format/bedrock.rb +6 -3
- data/lib/clacky/providers.rb +146 -3
- data/lib/clacky/server/channel/adapters/feishu/adapter.rb +14 -0
- data/lib/clacky/server/channel/adapters/feishu/bot.rb +10 -0
- data/lib/clacky/server/channel/adapters/feishu/message_parser.rb +1 -0
- data/lib/clacky/server/channel/channel_manager.rb +12 -4
- data/lib/clacky/server/channel/channel_ui_controller.rb +8 -2
- data/lib/clacky/server/http_server.rb +746 -13
- data/lib/clacky/server/session_registry.rb +55 -24
- data/lib/clacky/skill.rb +10 -9
- data/lib/clacky/skill_loader.rb +23 -11
- data/lib/clacky/tools/file_reader.rb +232 -127
- data/lib/clacky/tools/security.rb +42 -64
- data/lib/clacky/tools/terminal/persistent_session.rb +15 -4
- data/lib/clacky/tools/terminal/safe_rm.sh +106 -0
- data/lib/clacky/tools/terminal/session_manager.rb +8 -3
- data/lib/clacky/tools/terminal.rb +263 -16
- data/lib/clacky/ui2/layout_manager.rb +8 -1
- data/lib/clacky/ui2/output_buffer.rb +83 -23
- data/lib/clacky/ui2/ui_controller.rb +74 -7
- data/lib/clacky/utils/file_processor.rb +14 -40
- data/lib/clacky/utils/model_pricing.rb +215 -0
- data/lib/clacky/utils/parser_manager.rb +70 -6
- data/lib/clacky/utils/string_matcher.rb +23 -1
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +673 -9
- data/lib/clacky/web/app.js +40 -1608
- data/lib/clacky/web/i18n.js +209 -0
- data/lib/clacky/web/index.html +166 -2
- data/lib/clacky/web/onboard.js +77 -1
- data/lib/clacky/web/profile.js +442 -0
- data/lib/clacky/web/sessions.js +1034 -2
- data/lib/clacky/web/settings.js +127 -6
- data/lib/clacky/web/sidebar.js +39 -0
- data/lib/clacky/web/skills.js +460 -0
- data/lib/clacky/web/trash.js +343 -0
- data/lib/clacky/web/ws-dispatcher.js +255 -0
- data/lib/clacky.rb +5 -3
- metadata +16 -17
- data/lib/clacky/clacky_auth_client.rb +0 -152
- data/lib/clacky/clacky_cloud_config.rb +0 -123
- data/lib/clacky/cloud_project_client.rb +0 -169
- data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +0 -1377
- data/lib/clacky/default_skills/deploy/tools/check_health.rb +0 -116
- data/lib/clacky/default_skills/deploy/tools/create_database_service.rb +0 -341
- data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +0 -99
- data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +0 -77
- data/lib/clacky/default_skills/deploy/tools/list_services.rb +0 -67
- data/lib/clacky/default_skills/deploy/tools/report_deploy_status.rb +0 -67
- data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +0 -189
- data/lib/clacky/default_skills/new/scripts/cloud_project_init.sh +0 -74
- data/lib/clacky/deploy_api_client.rb +0 -484
|
@@ -1,484 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "faraday"
|
|
4
|
-
require "json"
|
|
5
|
-
|
|
6
|
-
module Clacky
|
|
7
|
-
# DeployApiClient - Encapsulates all Deploy API calls for the Railway deployment flow.
|
|
8
|
-
#
|
|
9
|
-
# All endpoints use Workspace API Key (clacky_ak_*) authentication, as the backend
|
|
10
|
-
# supports both clacky_ak_* and clacky_dk_* for all deploy endpoints.
|
|
11
|
-
#
|
|
12
|
-
# Usage:
|
|
13
|
-
# client = DeployApiClient.new("clacky_ak_xxx", base_url: "https://api.clacky.ai")
|
|
14
|
-
#
|
|
15
|
-
# # Check payment status
|
|
16
|
-
# result = client.payment_status(project_id: "proj_abc")
|
|
17
|
-
# # => { success: true, is_paid: true }
|
|
18
|
-
#
|
|
19
|
-
# # Create a deploy task
|
|
20
|
-
# result = client.create_task(project_id: "proj_abc")
|
|
21
|
-
# # => { success: true, deploy_task_id: "...", platform_token: "...", ... }
|
|
22
|
-
#
|
|
23
|
-
# # Poll services until DB is ready
|
|
24
|
-
# result = client.services(deploy_task_id: "task_abc")
|
|
25
|
-
# # => { success: true, services: [...], domain_name: "..." }
|
|
26
|
-
#
|
|
27
|
-
# # Poll deploy status
|
|
28
|
-
# result = client.deploy_status(deploy_task_id: "task_abc")
|
|
29
|
-
# # => { success: true, status: "SUCCESS", url: "https://..." }
|
|
30
|
-
#
|
|
31
|
-
# # Bind domain
|
|
32
|
-
# result = client.bind_domain(deploy_task_id: "task_abc")
|
|
33
|
-
# # => { success: true, domain: "my-app.example.com" }
|
|
34
|
-
#
|
|
35
|
-
# # Notify backend of deploy outcome
|
|
36
|
-
# client.notify(project_id: "...", deploy_task_id: "...", status: "success")
|
|
37
|
-
class DeployApiClient
|
|
38
|
-
BASE_PATH = "/openclacky/v1"
|
|
39
|
-
REQUEST_TIMEOUT = 30 # seconds for normal requests
|
|
40
|
-
OPEN_TIMEOUT = 10 # seconds for connection
|
|
41
|
-
|
|
42
|
-
def initialize(workspace_key, base_url:)
|
|
43
|
-
@workspace_key = workspace_key.to_s.strip
|
|
44
|
-
@base_url = base_url.to_s.strip.sub(%r{/+$}, "")
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# -------------------------------------------------------------------------
|
|
48
|
-
# Payment
|
|
49
|
-
# -------------------------------------------------------------------------
|
|
50
|
-
|
|
51
|
-
# Query whether the project has an active paid subscription.
|
|
52
|
-
#
|
|
53
|
-
# @param project_id [String]
|
|
54
|
-
# @return [Hash] { success: true, is_paid: Boolean } or { success: false, error: String }
|
|
55
|
-
def payment_status(project_id:)
|
|
56
|
-
response = connection.get("#{BASE_PATH}/deploy/payment") do |req|
|
|
57
|
-
req.params["project_id"] = project_id
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
return http_error(response) unless response.status == 200
|
|
61
|
-
|
|
62
|
-
body = parse_body(response)
|
|
63
|
-
return body_error(body) unless success_code?(body)
|
|
64
|
-
|
|
65
|
-
data = body["data"] || {}
|
|
66
|
-
{ success: true, is_paid: data["is_paid"] == true }
|
|
67
|
-
rescue Faraday::Error => e
|
|
68
|
-
{ success: false, error: "Network error: #{e.message}" }
|
|
69
|
-
rescue => e
|
|
70
|
-
{ success: false, error: "Unexpected error: #{e.message}" }
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# -------------------------------------------------------------------------
|
|
74
|
-
# Regions
|
|
75
|
-
# -------------------------------------------------------------------------
|
|
76
|
-
|
|
77
|
-
# Fetch the list of supported deployment regions.
|
|
78
|
-
#
|
|
79
|
-
# @param project_id [String] Required by the backend to scope region availability.
|
|
80
|
-
# @return [Hash] {
|
|
81
|
-
# success: true,
|
|
82
|
-
# regions: Array<Hash> # e.g. [{ "id" => "us-west", "name" => "US West", "label" => "US West (Oregon)" }, ...]
|
|
83
|
-
# } or { success: false, error: String }
|
|
84
|
-
def regions(project_id:)
|
|
85
|
-
response = connection.get("#{BASE_PATH}/deploy/regions") do |req|
|
|
86
|
-
req.params["project_id"] = project_id
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
return http_error(response) unless response.status == 200
|
|
90
|
-
|
|
91
|
-
body = parse_body(response)
|
|
92
|
-
return body_error(body) unless success_code?(body)
|
|
93
|
-
|
|
94
|
-
data = body["data"] || {}
|
|
95
|
-
list = data["regions"] || data || []
|
|
96
|
-
list = list.values if list.is_a?(Hash)
|
|
97
|
-
{ success: true, regions: Array(list) }
|
|
98
|
-
rescue Faraday::Error => e
|
|
99
|
-
{ success: false, error: "Network error: #{e.message}" }
|
|
100
|
-
rescue => e
|
|
101
|
-
{ success: false, error: "Unexpected error: #{e.message}" }
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
# -------------------------------------------------------------------------
|
|
105
|
-
# Create Deploy Task
|
|
106
|
-
# -------------------------------------------------------------------------
|
|
107
|
-
|
|
108
|
-
# Create a new deployment task on the backend. Returns Railway credentials.
|
|
109
|
-
#
|
|
110
|
-
# @param project_id [String]
|
|
111
|
-
# @param backup_db [Boolean] default false
|
|
112
|
-
# @param env_vars [Hash] extra env vars to pass at task creation time
|
|
113
|
-
# @param region [String] optional Railway region slug
|
|
114
|
-
# @return [Hash] {
|
|
115
|
-
# success: true,
|
|
116
|
-
# deploy_task_id: String,
|
|
117
|
-
# deploy_service_id: String,
|
|
118
|
-
# platform_token: String, # RAILWAY_TOKEN
|
|
119
|
-
# platform_project_id: String,
|
|
120
|
-
# platform_environment_id: String
|
|
121
|
-
# }
|
|
122
|
-
def create_task(project_id:, backup_db: false, env_vars: {}, region: nil)
|
|
123
|
-
body_params = { project_id: project_id, backup_db: backup_db }
|
|
124
|
-
body_params[:env_vars] = env_vars unless env_vars.empty?
|
|
125
|
-
body_params[:region] = region if region
|
|
126
|
-
|
|
127
|
-
response = connection.post("#{BASE_PATH}/deploy/create-task") do |req|
|
|
128
|
-
req.headers["Content-Type"] = "application/json"
|
|
129
|
-
req.body = JSON.generate(body_params)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
return http_error(response) unless response.status == 200
|
|
133
|
-
|
|
134
|
-
body = parse_body(response)
|
|
135
|
-
return body_error(body) unless success_code?(body)
|
|
136
|
-
|
|
137
|
-
data = body["data"] || {}
|
|
138
|
-
{
|
|
139
|
-
success: true,
|
|
140
|
-
deploy_task_id: data["deploy_task_id"],
|
|
141
|
-
deploy_service_id: data["deploy_service_id"],
|
|
142
|
-
platform_token: data["platform_token"],
|
|
143
|
-
platform_project_id: data["platform_project_id"],
|
|
144
|
-
platform_environment_id: data["platform_environment_id"]
|
|
145
|
-
}
|
|
146
|
-
rescue Faraday::Error => e
|
|
147
|
-
{ success: false, error: "Network error: #{e.message}" }
|
|
148
|
-
rescue => e
|
|
149
|
-
{ success: false, error: "Unexpected error: #{e.message}" }
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
# -------------------------------------------------------------------------
|
|
153
|
-
# Services (poll for middleware readiness)
|
|
154
|
-
# -------------------------------------------------------------------------
|
|
155
|
-
|
|
156
|
-
# Query all services under a deploy task.
|
|
157
|
-
# Used to wait for the PostgreSQL middleware to reach status SUCCESS
|
|
158
|
-
# before injecting the DATABASE_URL reference.
|
|
159
|
-
#
|
|
160
|
-
# @param deploy_task_id [String]
|
|
161
|
-
# @return [Hash] {
|
|
162
|
-
# success: true,
|
|
163
|
-
# services: Array<Hash>, # full service objects from API
|
|
164
|
-
# domain_name: String, # assigned domain (may be empty on first call)
|
|
165
|
-
# db_service: Hash | nil # first middleware service with status SUCCESS
|
|
166
|
-
# }
|
|
167
|
-
def services(deploy_task_id:)
|
|
168
|
-
url = "#{BASE_PATH}/deploy/services?deploy_task_id=#{deploy_task_id}"
|
|
169
|
-
puts " [DEBUG API] GET #{@base_url}#{url}"
|
|
170
|
-
|
|
171
|
-
response = connection.get("#{BASE_PATH}/deploy/services") do |req|
|
|
172
|
-
req.params["deploy_task_id"] = deploy_task_id
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
puts " [DEBUG API] Response status: #{response.status}"
|
|
176
|
-
|
|
177
|
-
return http_error(response) unless response.status == 200
|
|
178
|
-
|
|
179
|
-
body = parse_body(response)
|
|
180
|
-
puts " [DEBUG API] Response body: #{body.inspect[0..500]}..." if body
|
|
181
|
-
|
|
182
|
-
return body_error(body) unless success_code?(body)
|
|
183
|
-
|
|
184
|
-
data = body["data"] || {}
|
|
185
|
-
svcs = data["services"] || []
|
|
186
|
-
domain = data["domain_name"].to_s
|
|
187
|
-
|
|
188
|
-
# Debug: print detailed service info
|
|
189
|
-
puts " [DEBUG] Total services returned: #{svcs.size}"
|
|
190
|
-
svcs.each_with_index do |s, idx|
|
|
191
|
-
puts " [DEBUG] Service[#{idx}]: name=#{s['service_name']}, type=#{s['type']}, status=#{s['status']}"
|
|
192
|
-
if s["type"] == "middleware"
|
|
193
|
-
env_vars = s["env_vars"] || {}
|
|
194
|
-
puts " [DEBUG] - env_vars keys: #{env_vars.keys.join(', ')}"
|
|
195
|
-
puts " [DEBUG] - has DATABASE_URL: #{env_vars.key?('DATABASE_URL')}"
|
|
196
|
-
puts " [DEBUG] - has DATABASE_PUBLIC_URL: #{env_vars.key?('DATABASE_PUBLIC_URL')}"
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
# Find first middleware (DB) that is fully provisioned
|
|
201
|
-
db_svc = svcs.find do |s|
|
|
202
|
-
s["type"] == "middleware" && s["status"]&.upcase == "SUCCESS"
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
puts " [DEBUG] db_svc found: #{!db_svc.nil?}"
|
|
206
|
-
if db_svc
|
|
207
|
-
puts " [DEBUG] - db_svc name: #{db_svc['service_name']}"
|
|
208
|
-
puts " [DEBUG] - db_svc status: #{db_svc['status']}"
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
# middleware_support: { supported: Boolean, supported_types: Array }
|
|
212
|
-
# When supported == false, no DB middleware will be provisioned by Clacky.
|
|
213
|
-
# The deploy script uses this to skip the DB polling loop entirely.
|
|
214
|
-
middleware_support = data["middleware_support"] || {}
|
|
215
|
-
puts " [DEBUG] middleware_support: #{middleware_support.inspect}"
|
|
216
|
-
|
|
217
|
-
# platform_bucket_credentials contains S3-compatible storage credentials.
|
|
218
|
-
# Passed through so the deploy script can inject STORAGE_BUCKET_* env vars.
|
|
219
|
-
bucket_credentials = data["platform_bucket_credentials"]
|
|
220
|
-
bucket_name = data["platform_bucket_name"].to_s
|
|
221
|
-
|
|
222
|
-
{
|
|
223
|
-
success: true,
|
|
224
|
-
services: svcs,
|
|
225
|
-
domain_name: domain,
|
|
226
|
-
db_service: db_svc,
|
|
227
|
-
middleware_support: middleware_support,
|
|
228
|
-
bucket_credentials: bucket_credentials,
|
|
229
|
-
bucket_name: bucket_name
|
|
230
|
-
}
|
|
231
|
-
rescue Faraday::Error => e
|
|
232
|
-
{ success: false, error: "Network error: #{e.message}" }
|
|
233
|
-
rescue => e
|
|
234
|
-
{ success: false, error: "Unexpected error: #{e.message}" }
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
# -------------------------------------------------------------------------
|
|
238
|
-
# Deploy Status
|
|
239
|
-
# -------------------------------------------------------------------------
|
|
240
|
-
|
|
241
|
-
# Query the real-time deployment status for a task.
|
|
242
|
-
#
|
|
243
|
-
# @param deploy_task_id [String]
|
|
244
|
-
# @return [Hash] {
|
|
245
|
-
# success: true,
|
|
246
|
-
# status: String, # SUCCESS / FAILED / CRASHED / DEPLOYING / WAITING
|
|
247
|
-
# url: String
|
|
248
|
-
# }
|
|
249
|
-
def deploy_status(deploy_task_id:)
|
|
250
|
-
response = connection.get("#{BASE_PATH}/deploy/status") do |req|
|
|
251
|
-
req.params["deploy_task_id"] = deploy_task_id
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
return http_error(response) unless response.status == 200
|
|
255
|
-
|
|
256
|
-
body = parse_body(response)
|
|
257
|
-
return body_error(body) unless success_code?(body)
|
|
258
|
-
|
|
259
|
-
data = body["data"] || {}
|
|
260
|
-
{
|
|
261
|
-
success: true,
|
|
262
|
-
status: data["status"].to_s.upcase,
|
|
263
|
-
url: data["url"].to_s,
|
|
264
|
-
deploy_service_id: data["deploy_service_id"].to_s
|
|
265
|
-
}
|
|
266
|
-
rescue Faraday::Error => e
|
|
267
|
-
{ success: false, error: "Network error: #{e.message}" }
|
|
268
|
-
rescue => e
|
|
269
|
-
{ success: false, error: "Unexpected error: #{e.message}" }
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
# -------------------------------------------------------------------------
|
|
273
|
-
# Bind Domain
|
|
274
|
-
# -------------------------------------------------------------------------
|
|
275
|
-
|
|
276
|
-
# Bind a custom domain to the deploy task.
|
|
277
|
-
#
|
|
278
|
-
# @param deploy_task_id [String]
|
|
279
|
-
# @return [Hash] { success: true, domain: String } or { success: false, error: String }
|
|
280
|
-
def bind_domain(deploy_task_id:)
|
|
281
|
-
response = connection.post("#{BASE_PATH}/deploy/bind-domain") do |req|
|
|
282
|
-
req.headers["Content-Type"] = "application/json"
|
|
283
|
-
req.body = JSON.generate({ deploy_task_id: deploy_task_id })
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
return http_error(response) unless response.status == 200
|
|
287
|
-
|
|
288
|
-
body = parse_body(response)
|
|
289
|
-
return body_error(body) unless success_code?(body)
|
|
290
|
-
|
|
291
|
-
data = body["data"] || {}
|
|
292
|
-
{ success: true, domain: data["domain"].to_s }
|
|
293
|
-
rescue Faraday::Error => e
|
|
294
|
-
{ success: false, error: "Network error: #{e.message}" }
|
|
295
|
-
rescue => e
|
|
296
|
-
{ success: false, error: "Unexpected error: #{e.message}" }
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
# -------------------------------------------------------------------------
|
|
300
|
-
# Build Logs
|
|
301
|
-
# -------------------------------------------------------------------------
|
|
302
|
-
|
|
303
|
-
# Fetch build logs for a deploy task (synchronous, not SSE).
|
|
304
|
-
#
|
|
305
|
-
# @param deploy_task_id [String]
|
|
306
|
-
# @param service_id [String, nil] optional service ID filter
|
|
307
|
-
# @param level [String] log level filter ("INFO", "ERROR", "WARN", etc.)
|
|
308
|
-
# @param lines [Integer] maximum number of lines to return (default: 100)
|
|
309
|
-
# @return [Hash] {
|
|
310
|
-
# success: true,
|
|
311
|
-
# logs: Array<Hash> # [{ "timestamp" => ..., "level" => "INFO", "message" => "..." }, ...]
|
|
312
|
-
# } or { success: false, error: String }
|
|
313
|
-
def build_logs(deploy_task_id:, service_id: nil, level: "INFO", lines: 100)
|
|
314
|
-
body_params = {
|
|
315
|
-
deploy_task_id: deploy_task_id,
|
|
316
|
-
level: level,
|
|
317
|
-
lines: lines
|
|
318
|
-
}
|
|
319
|
-
body_params[:service_id] = service_id if service_id
|
|
320
|
-
|
|
321
|
-
response = connection.post("#{BASE_PATH}/tasks/logs") do |req|
|
|
322
|
-
req.headers["Content-Type"] = "application/json"
|
|
323
|
-
req.body = JSON.generate(body_params)
|
|
324
|
-
end
|
|
325
|
-
|
|
326
|
-
return http_error(response) unless response.status == 200
|
|
327
|
-
|
|
328
|
-
body = parse_body(response)
|
|
329
|
-
return body_error(body) unless success_code?(body)
|
|
330
|
-
|
|
331
|
-
data = body["data"] || {}
|
|
332
|
-
logs = data["logs"] || []
|
|
333
|
-
{ success: true, logs: logs }
|
|
334
|
-
rescue Faraday::Error => e
|
|
335
|
-
{ success: false, error: "Network error: #{e.message}" }
|
|
336
|
-
rescue => e
|
|
337
|
-
{ success: false, error: "Unexpected error: #{e.message}" }
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
# Stream build logs using SSE (Server-Sent Events).
|
|
341
|
-
# This method yields each log line as it arrives.
|
|
342
|
-
#
|
|
343
|
-
# @param deploy_task_id [String]
|
|
344
|
-
# @param service_id [String, nil] optional service ID filter
|
|
345
|
-
# @param level [String] log level filter ("INFO", "ERROR", "WARN", etc.)
|
|
346
|
-
# @yield [Hash] each log event: { "type" => "log", "timestamp" => ..., "message" => "..." }
|
|
347
|
-
# @return [Hash] { success: true } or { success: false, error: String }
|
|
348
|
-
def stream_build_logs(deploy_task_id:, service_id: nil, level: "INFO", &block)
|
|
349
|
-
require "net/http"
|
|
350
|
-
require "openssl"
|
|
351
|
-
|
|
352
|
-
body_params = {
|
|
353
|
-
deploy_task_id: deploy_task_id,
|
|
354
|
-
level: level
|
|
355
|
-
}
|
|
356
|
-
body_params[:service_id] = service_id if service_id
|
|
357
|
-
|
|
358
|
-
url = "#{@base_url}#{BASE_PATH}/tasks/stream/build-logs"
|
|
359
|
-
uri = URI.parse(url)
|
|
360
|
-
|
|
361
|
-
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http|
|
|
362
|
-
request = Net::HTTP::Post.new(uri.path)
|
|
363
|
-
request["Authorization"] = "Bearer #{@workspace_key}"
|
|
364
|
-
request["Accept"] = "text/event-stream"
|
|
365
|
-
request["Content-Type"] = "application/json"
|
|
366
|
-
request.body = JSON.generate(body_params)
|
|
367
|
-
|
|
368
|
-
http.request(request) do |response|
|
|
369
|
-
return { success: false, error: "HTTP #{response.code}: #{response.message}" } unless response.code.to_i == 200
|
|
370
|
-
|
|
371
|
-
buffer = ""
|
|
372
|
-
response.read_body do |chunk|
|
|
373
|
-
buffer << chunk
|
|
374
|
-
while (line_end = buffer.index("\n"))
|
|
375
|
-
line = buffer.slice!(0..line_end).strip
|
|
376
|
-
next if line.empty? || !line.start_with?("data:")
|
|
377
|
-
|
|
378
|
-
json_str = line.sub(/^data:\s*/, "")
|
|
379
|
-
begin
|
|
380
|
-
event = JSON.parse(json_str)
|
|
381
|
-
block.call(event) if block
|
|
382
|
-
rescue JSON::ParserError
|
|
383
|
-
# Ignore malformed JSON
|
|
384
|
-
end
|
|
385
|
-
end
|
|
386
|
-
end
|
|
387
|
-
end
|
|
388
|
-
end
|
|
389
|
-
|
|
390
|
-
{ success: true }
|
|
391
|
-
rescue => e
|
|
392
|
-
{ success: false, error: "Stream error: #{e.message}" }
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
# -------------------------------------------------------------------------
|
|
396
|
-
# Notify
|
|
397
|
-
# -------------------------------------------------------------------------
|
|
398
|
-
|
|
399
|
-
# Notify the backend of the current deployment outcome.
|
|
400
|
-
# Fire-and-forget — failures are logged but do not raise.
|
|
401
|
-
#
|
|
402
|
-
# @param project_id [String]
|
|
403
|
-
# @param deploy_task_id [String]
|
|
404
|
-
# @param deploy_service_id [String] optional
|
|
405
|
-
# @param status [String] "deploying" | "success" | "failed"
|
|
406
|
-
# @param message [String] optional description
|
|
407
|
-
# @param target_port [Integer] default 3000
|
|
408
|
-
# @return [Hash] { success: true } or { success: false, error: String }
|
|
409
|
-
def notify(project_id:, deploy_task_id:, status:,
|
|
410
|
-
deploy_service_id: nil, message: nil, target_port: nil)
|
|
411
|
-
payload = {
|
|
412
|
-
project_id: project_id,
|
|
413
|
-
deploy_task_id: deploy_task_id,
|
|
414
|
-
status: status
|
|
415
|
-
}
|
|
416
|
-
payload[:deploy_service_id] = deploy_service_id if deploy_service_id
|
|
417
|
-
payload[:message] = message if message
|
|
418
|
-
payload[:target_port] = target_port if target_port
|
|
419
|
-
|
|
420
|
-
response = connection.post("#{BASE_PATH}/deploy/notify") do |req|
|
|
421
|
-
req.headers["Content-Type"] = "application/json"
|
|
422
|
-
req.body = JSON.generate(payload)
|
|
423
|
-
end
|
|
424
|
-
|
|
425
|
-
return http_error(response) unless response.status == 200
|
|
426
|
-
|
|
427
|
-
body = parse_body(response)
|
|
428
|
-
return body_error(body) unless success_code?(body)
|
|
429
|
-
|
|
430
|
-
{ success: true }
|
|
431
|
-
rescue => e
|
|
432
|
-
# Notify failures are non-fatal — log and move on
|
|
433
|
-
warn "[deploy_api] notify failed: #{e.message}"
|
|
434
|
-
{ success: false, error: e.message }
|
|
435
|
-
end
|
|
436
|
-
|
|
437
|
-
# -------------------------------------------------------------------------
|
|
438
|
-
# Private helpers
|
|
439
|
-
# -------------------------------------------------------------------------
|
|
440
|
-
|
|
441
|
-
private def connection
|
|
442
|
-
@connection ||= Faraday.new(url: @base_url) do |f|
|
|
443
|
-
f.options.timeout = REQUEST_TIMEOUT
|
|
444
|
-
f.options.open_timeout = OPEN_TIMEOUT
|
|
445
|
-
f.headers["Authorization"] = "Bearer #{@workspace_key}"
|
|
446
|
-
f.headers["Accept"] = "application/json"
|
|
447
|
-
# Disable SSL verification to avoid OpenSSL certificate path issues
|
|
448
|
-
# on some macOS environments with system Ruby
|
|
449
|
-
f.ssl.verify = false
|
|
450
|
-
f.adapter Faraday.default_adapter
|
|
451
|
-
end
|
|
452
|
-
end
|
|
453
|
-
|
|
454
|
-
private def parse_body(response)
|
|
455
|
-
JSON.parse(response.body)
|
|
456
|
-
rescue JSON::ParserError
|
|
457
|
-
nil
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
private def success_code?(body)
|
|
461
|
-
return false if body.nil?
|
|
462
|
-
|
|
463
|
-
code = body["code"].to_i
|
|
464
|
-
code == 0 || code == 200
|
|
465
|
-
end
|
|
466
|
-
|
|
467
|
-
private def http_error(response)
|
|
468
|
-
msg = begin
|
|
469
|
-
parsed = JSON.parse(response.body)
|
|
470
|
-
parsed["message"] || parsed["msg"] || response.body.to_s[0, 200]
|
|
471
|
-
rescue
|
|
472
|
-
response.body.to_s[0, 200]
|
|
473
|
-
end
|
|
474
|
-
{ success: false, error: "HTTP #{response.status}: #{msg}" }
|
|
475
|
-
end
|
|
476
|
-
|
|
477
|
-
private def body_error(body)
|
|
478
|
-
return { success: false, error: "Invalid JSON response from API" } if body.nil?
|
|
479
|
-
|
|
480
|
-
msg = body["message"] || body["msg"] || "Unknown API error (code: #{body["code"]})"
|
|
481
|
-
{ success: false, error: msg }
|
|
482
|
-
end
|
|
483
|
-
end
|
|
484
|
-
end
|