openclacky 0.9.28 → 0.9.30
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/docs/deploy-architecture.md +619 -0
- data/lib/clacky/agent/llm_caller.rb +14 -2
- data/lib/clacky/agent/message_compressor.rb +24 -6
- data/lib/clacky/agent/message_compressor_helper.rb +17 -10
- data/lib/clacky/agent/session_serializer.rb +69 -0
- data/lib/clacky/agent/skill_manager.rb +2 -2
- data/lib/clacky/agent.rb +3 -0
- data/lib/clacky/brand_config.rb +29 -3
- data/lib/clacky/clacky_auth_client.rb +152 -0
- data/lib/clacky/clacky_cloud_config.rb +123 -0
- data/lib/clacky/cli.rb +13 -0
- data/lib/clacky/client.rb +21 -7
- data/lib/clacky/cloud_project_client.rb +169 -0
- data/lib/clacky/default_agents/base_prompt.md +1 -0
- data/lib/clacky/default_parsers/doc_parser.rb +9 -9
- data/lib/clacky/default_skills/browser-setup/SKILL.md +9 -0
- data/lib/clacky/default_skills/channel-setup/SKILL.md +21 -4
- data/lib/clacky/default_skills/channel-setup/feishu_setup.rb +8 -2
- data/lib/clacky/default_skills/deploy/SKILL.md +96 -5
- data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +1268 -274
- data/lib/clacky/default_skills/deploy/tools/create_database_service.rb +341 -0
- data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +72 -147
- data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +60 -50
- data/lib/clacky/default_skills/deploy/tools/list_services.rb +47 -60
- data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +147 -96
- data/lib/clacky/default_skills/new/SKILL.md +117 -5
- data/lib/clacky/default_skills/new/scripts/cloud_project_init.sh +74 -0
- data/lib/clacky/default_skills/new/scripts/create_rails_project.sh +32 -0
- data/lib/clacky/deploy_api_client.rb +484 -0
- data/lib/clacky/json_ui_controller.rb +16 -10
- data/lib/clacky/message_format/bedrock.rb +3 -2
- data/lib/clacky/message_history.rb +8 -0
- data/lib/clacky/plain_ui_controller.rb +1 -6
- data/lib/clacky/providers.rb +23 -4
- data/lib/clacky/server/browser_manager.rb +3 -1
- data/lib/clacky/server/channel/adapters/feishu/ws_client.rb +2 -1
- data/lib/clacky/server/channel/adapters/wecom/ws_client.rb +3 -1
- data/lib/clacky/server/channel/adapters/weixin/adapter.rb +5 -5
- data/lib/clacky/server/http_server.rb +12 -2
- data/lib/clacky/server/server_master.rb +43 -7
- data/lib/clacky/server/web_ui_controller.rb +17 -9
- data/lib/clacky/skill.rb +6 -2
- data/lib/clacky/tools/run_project.rb +4 -1
- data/lib/clacky/tools/shell.rb +7 -1
- data/lib/clacky/ui2/ui_controller.rb +1 -5
- data/lib/clacky/ui_interface.rb +5 -7
- data/lib/clacky/utils/arguments_parser.rb +22 -5
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +45 -5
- data/lib/clacky/web/app.js +126 -19
- data/lib/clacky/web/i18n.js +57 -0
- data/lib/clacky/web/sessions.js +108 -39
- data/lib/clacky/web/skills.js +8 -2
- data/lib/clacky.rb +3 -0
- metadata +8 -1
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
|
|
6
|
+
module Clacky
|
|
7
|
+
module DeployTools
|
|
8
|
+
# Create a PostgreSQL database service on Railway and wait for it to be ready.
|
|
9
|
+
#
|
|
10
|
+
# This tool handles the known Railway CLI bug where `railway add` returns an
|
|
11
|
+
# error exit code when using project tokens, but actually succeeds in creating
|
|
12
|
+
# the service.
|
|
13
|
+
#
|
|
14
|
+
# Strategy:
|
|
15
|
+
# 1. Capture existing service IDs before creation
|
|
16
|
+
# 2. Execute `railway add --database postgres` (ignore exit code)
|
|
17
|
+
# 3. Poll for new service ID (max 15 seconds)
|
|
18
|
+
# 4. Wait for DATABASE_URL to be available on the new service (max 2 minutes)
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# result = CreateDatabaseService.execute(
|
|
22
|
+
# platform_token: "railway-token-here"
|
|
23
|
+
# )
|
|
24
|
+
# if result[:success]
|
|
25
|
+
# puts "Database created: #{result[:service_name]}"
|
|
26
|
+
# puts "DATABASE_URL: #{result[:database_url]}"
|
|
27
|
+
# end
|
|
28
|
+
class CreateDatabaseService
|
|
29
|
+
|
|
30
|
+
# Maximum time to wait for new service to appear after creation command
|
|
31
|
+
DETECTION_TIMEOUT = 15 # seconds
|
|
32
|
+
DETECTION_INTERVAL = 3 # seconds
|
|
33
|
+
|
|
34
|
+
# Maximum time to wait for DATABASE_URL to be available
|
|
35
|
+
PROVISION_TIMEOUT = 120 # seconds (2 minutes)
|
|
36
|
+
PROVISION_INTERVAL = 5 # seconds
|
|
37
|
+
|
|
38
|
+
# Execute database creation
|
|
39
|
+
#
|
|
40
|
+
# @param platform_token [String] RAILWAY_TOKEN for authentication
|
|
41
|
+
# @return [Hash] {
|
|
42
|
+
# success: Boolean,
|
|
43
|
+
# service_id: String (if success),
|
|
44
|
+
# service_name: String (if success),
|
|
45
|
+
# database_url: String (if success),
|
|
46
|
+
# error: String (if failed)
|
|
47
|
+
# }
|
|
48
|
+
def self.execute(platform_token:)
|
|
49
|
+
new(platform_token).execute
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def initialize(platform_token)
|
|
53
|
+
@platform_token = platform_token
|
|
54
|
+
@env = { "RAILWAY_TOKEN" => platform_token }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def execute
|
|
58
|
+
# Step 0: Check if database already exists
|
|
59
|
+
existing_db = find_existing_database
|
|
60
|
+
|
|
61
|
+
if existing_db
|
|
62
|
+
puts " ✅ Database already exists: #{existing_db[:name]}"
|
|
63
|
+
|
|
64
|
+
# Return without DATABASE_URL - Railway automatically shares it across services
|
|
65
|
+
return {
|
|
66
|
+
success: true,
|
|
67
|
+
service_id: existing_db[:id],
|
|
68
|
+
service_name: existing_db[:name],
|
|
69
|
+
database_url: nil, # Don't fetch - Railway auto-injects
|
|
70
|
+
status: "existing"
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# No database exists - create new one
|
|
75
|
+
puts " 📦 Creating new PostgreSQL database..."
|
|
76
|
+
|
|
77
|
+
# Step 1: Get existing service IDs
|
|
78
|
+
existing_service_ids = fetch_service_ids
|
|
79
|
+
|
|
80
|
+
unless existing_service_ids
|
|
81
|
+
return { success: false, error: "Failed to fetch existing services" }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Step 2: Execute create command (ignore exit code due to known bug)
|
|
85
|
+
execute_create_command
|
|
86
|
+
|
|
87
|
+
# Step 3: Detect new service
|
|
88
|
+
new_service = detect_new_service(existing_service_ids)
|
|
89
|
+
|
|
90
|
+
unless new_service
|
|
91
|
+
return { success: false, error: "Database service not detected after #{DETECTION_TIMEOUT}s" }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Step 4: Wait for DATABASE_URL
|
|
95
|
+
database_url = wait_for_database_url(new_service[:name])
|
|
96
|
+
|
|
97
|
+
unless database_url
|
|
98
|
+
return {
|
|
99
|
+
success: false,
|
|
100
|
+
error: "DATABASE_URL not available after #{PROVISION_TIMEOUT}s for service #{new_service[:name]}"
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
{
|
|
105
|
+
success: true,
|
|
106
|
+
service_id: new_service[:id],
|
|
107
|
+
service_name: new_service[:name],
|
|
108
|
+
database_url: database_url,
|
|
109
|
+
status: "created"
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
rescue => e
|
|
113
|
+
{
|
|
114
|
+
success: false,
|
|
115
|
+
error: "Unexpected error: #{e.message}"
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Find existing Postgres database service
|
|
120
|
+
# @return [Hash, nil] { id:, name: } or nil if not found
|
|
121
|
+
private def find_existing_database
|
|
122
|
+
Tempfile.create('railway_status') do |tmpfile|
|
|
123
|
+
success = system(
|
|
124
|
+
@env,
|
|
125
|
+
"railway", "status", "--json",
|
|
126
|
+
in: :close,
|
|
127
|
+
out: tmpfile,
|
|
128
|
+
err: File::NULL
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return nil unless success
|
|
132
|
+
|
|
133
|
+
tmpfile.rewind
|
|
134
|
+
output = tmpfile.read.strip
|
|
135
|
+
return nil if output.empty?
|
|
136
|
+
|
|
137
|
+
begin
|
|
138
|
+
data = JSON.parse(output)
|
|
139
|
+
|
|
140
|
+
# Navigate to production environment's service instances
|
|
141
|
+
edges = data.dig("environments", "edges")
|
|
142
|
+
return nil unless edges&.any?
|
|
143
|
+
|
|
144
|
+
prod_env = edges.find { |e| e.dig("node", "name") == "production" }
|
|
145
|
+
return nil unless prod_env
|
|
146
|
+
|
|
147
|
+
service_edges = prod_env.dig("node", "serviceInstances", "edges")
|
|
148
|
+
return nil unless service_edges
|
|
149
|
+
|
|
150
|
+
# Find Postgres service (by name pattern or image source)
|
|
151
|
+
postgres_edge = service_edges.find do |edge|
|
|
152
|
+
node = edge["node"]
|
|
153
|
+
next false unless node
|
|
154
|
+
|
|
155
|
+
service_name = node["serviceName"].to_s
|
|
156
|
+
source_image = node.dig("source", "image").to_s
|
|
157
|
+
|
|
158
|
+
# Match: name contains "Postgres" or "postgres", or source is postgres image
|
|
159
|
+
service_name.match?(/postgres/i) || source_image.include?("postgres")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
return nil unless postgres_edge
|
|
163
|
+
|
|
164
|
+
node = postgres_edge["node"]
|
|
165
|
+
{ id: node["serviceId"], name: node["serviceName"] }
|
|
166
|
+
rescue JSON::ParserError
|
|
167
|
+
nil
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Fetch all existing service IDs from railway status
|
|
173
|
+
# @return [Array<String>, nil] Array of service IDs, or nil on failure
|
|
174
|
+
private def fetch_service_ids
|
|
175
|
+
Tempfile.create('railway_status') do |tmpfile|
|
|
176
|
+
success = system(
|
|
177
|
+
@env,
|
|
178
|
+
"railway", "status", "--json",
|
|
179
|
+
in: :close,
|
|
180
|
+
out: tmpfile,
|
|
181
|
+
err: File::NULL
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return nil unless success
|
|
185
|
+
|
|
186
|
+
tmpfile.rewind
|
|
187
|
+
output = tmpfile.read.strip
|
|
188
|
+
return [] if output.empty?
|
|
189
|
+
|
|
190
|
+
begin
|
|
191
|
+
data = JSON.parse(output)
|
|
192
|
+
|
|
193
|
+
# Extract service IDs from environments.edges[0].node.serviceInstances.edges
|
|
194
|
+
edges = data.dig("environments", "edges")
|
|
195
|
+
return [] unless edges&.any?
|
|
196
|
+
|
|
197
|
+
service_instances = edges[0].dig("node", "serviceInstances", "edges")
|
|
198
|
+
return [] unless service_instances
|
|
199
|
+
|
|
200
|
+
service_instances.map { |edge| edge.dig("node", "serviceId") }.compact
|
|
201
|
+
rescue JSON::ParserError
|
|
202
|
+
nil
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Execute the railway add command
|
|
208
|
+
# NOTE: We ignore the exit code because Railway CLI has a known bug
|
|
209
|
+
# where it returns error with project tokens but still succeeds
|
|
210
|
+
private def execute_create_command
|
|
211
|
+
system(
|
|
212
|
+
@env,
|
|
213
|
+
"railway", "add", "--database", "postgres",
|
|
214
|
+
in: :close,
|
|
215
|
+
out: File::NULL,
|
|
216
|
+
err: File::NULL
|
|
217
|
+
)
|
|
218
|
+
# Exit code intentionally ignored
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Detect newly created service by comparing service IDs
|
|
222
|
+
# @param existing_ids [Array<String>] Service IDs before creation
|
|
223
|
+
# @return [Hash, nil] { id: String, name: String } or nil if not found
|
|
224
|
+
private def detect_new_service(existing_ids)
|
|
225
|
+
max_attempts = DETECTION_TIMEOUT / DETECTION_INTERVAL
|
|
226
|
+
|
|
227
|
+
max_attempts.times do
|
|
228
|
+
sleep DETECTION_INTERVAL
|
|
229
|
+
|
|
230
|
+
current_ids = fetch_service_ids
|
|
231
|
+
next unless current_ids
|
|
232
|
+
|
|
233
|
+
new_ids = current_ids - existing_ids
|
|
234
|
+
|
|
235
|
+
if new_ids.any?
|
|
236
|
+
# Fetch full service info to get the name
|
|
237
|
+
return fetch_service_info(new_ids.first)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
nil
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Fetch service name for a given service ID
|
|
245
|
+
# @param service_id [String]
|
|
246
|
+
# @return [Hash, nil] { id: String, name: String }
|
|
247
|
+
private def fetch_service_info(service_id)
|
|
248
|
+
Tempfile.create('railway_status') do |tmpfile|
|
|
249
|
+
success = system(
|
|
250
|
+
@env,
|
|
251
|
+
"railway", "status", "--json",
|
|
252
|
+
in: :close,
|
|
253
|
+
out: tmpfile,
|
|
254
|
+
err: File::NULL
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
return nil unless success
|
|
258
|
+
|
|
259
|
+
tmpfile.rewind
|
|
260
|
+
output = tmpfile.read.strip
|
|
261
|
+
return nil if output.empty?
|
|
262
|
+
|
|
263
|
+
begin
|
|
264
|
+
data = JSON.parse(output)
|
|
265
|
+
edges = data.dig("environments", "edges")
|
|
266
|
+
return nil unless edges&.any?
|
|
267
|
+
|
|
268
|
+
service_instances = edges[0].dig("node", "serviceInstances", "edges")
|
|
269
|
+
return nil unless service_instances
|
|
270
|
+
|
|
271
|
+
# Find the service with matching ID
|
|
272
|
+
service_node = service_instances.find do |edge|
|
|
273
|
+
edge.dig("node", "serviceId") == service_id
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
return nil unless service_node
|
|
277
|
+
|
|
278
|
+
{
|
|
279
|
+
id: service_id,
|
|
280
|
+
name: service_node.dig("node", "serviceName")
|
|
281
|
+
}
|
|
282
|
+
rescue JSON::ParserError
|
|
283
|
+
nil
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Wait for DATABASE_URL to be available on the service
|
|
289
|
+
# @param service_name [String]
|
|
290
|
+
# @return [String, nil] DATABASE_URL or nil if timeout
|
|
291
|
+
private def wait_for_database_url(service_name)
|
|
292
|
+
max_attempts = PROVISION_TIMEOUT / PROVISION_INTERVAL
|
|
293
|
+
|
|
294
|
+
max_attempts.times do
|
|
295
|
+
sleep PROVISION_INTERVAL
|
|
296
|
+
|
|
297
|
+
database_url = fetch_database_url(service_name)
|
|
298
|
+
return database_url if database_url
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
nil
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Fetch DATABASE_URL from service variables
|
|
305
|
+
# @param service_name [String]
|
|
306
|
+
# @return [String, nil] DATABASE_URL value or nil
|
|
307
|
+
private def fetch_database_url(service_name)
|
|
308
|
+
tmpfile = Tempfile.new(['railway_vars', '.json'])
|
|
309
|
+
|
|
310
|
+
begin
|
|
311
|
+
success = system(
|
|
312
|
+
@env,
|
|
313
|
+
"railway", "variables", "--service", service_name, "--json",
|
|
314
|
+
in: :close,
|
|
315
|
+
out: tmpfile,
|
|
316
|
+
err: File::NULL
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
return nil unless success
|
|
320
|
+
|
|
321
|
+
tmpfile.rewind
|
|
322
|
+
output = tmpfile.read.strip
|
|
323
|
+
return nil if output.empty?
|
|
324
|
+
|
|
325
|
+
begin
|
|
326
|
+
# Output should be a JSON hash of variables
|
|
327
|
+
vars = JSON.parse(output)
|
|
328
|
+
|
|
329
|
+
# Prefer DATABASE_URL, fall back to DATABASE_PUBLIC_URL
|
|
330
|
+
vars["DATABASE_URL"] || vars["DATABASE_PUBLIC_URL"]
|
|
331
|
+
rescue JSON::ParserError
|
|
332
|
+
nil
|
|
333
|
+
end
|
|
334
|
+
ensure
|
|
335
|
+
tmpfile.close
|
|
336
|
+
tmpfile.unlink
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
@@ -1,173 +1,98 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'json'
|
|
4
|
-
|
|
5
3
|
module Clacky
|
|
6
4
|
module DeployTools
|
|
7
|
-
#
|
|
5
|
+
# Trigger a Railway deployment via `railway up` (blocking, with live logs).
|
|
6
|
+
# Uses RAILWAY_TOKEN passed through environment — no clackycli wrapper needed.
|
|
8
7
|
class ExecuteDeployment
|
|
9
|
-
MAX_WAIT_TIME = 600 # 10 minutes
|
|
10
|
-
POLL_INTERVAL = 5 # 5 seconds
|
|
11
8
|
|
|
12
|
-
#
|
|
9
|
+
# Trigger deployment for a service (blocking - waits for completion).
|
|
13
10
|
#
|
|
14
|
-
# @param service_name
|
|
15
|
-
# @
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
details: "Please provide a valid service name"
|
|
21
|
-
}
|
|
11
|
+
# @param service_name [String] Railway service name (from railway.toml or service list)
|
|
12
|
+
# @param platform_token [String] RAILWAY_TOKEN for this deploy task
|
|
13
|
+
# @return [Hash] { success: true, url: String } or { success: false, error: String }
|
|
14
|
+
def self.execute(service_name:, platform_token:)
|
|
15
|
+
if service_name.nil? || service_name.strip.empty?
|
|
16
|
+
return { success: false, error: "service_name is required" }
|
|
22
17
|
end
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
# Trigger deployment
|
|
27
|
-
command = "clackycli up -s #{shell_escape(service_name)} -d"
|
|
28
|
-
output = `#{command} 2>&1`
|
|
29
|
-
exit_code = $?.exitstatus
|
|
30
|
-
|
|
31
|
-
if exit_code != 0
|
|
32
|
-
return {
|
|
33
|
-
error: "Failed to trigger deployment",
|
|
34
|
-
details: output,
|
|
35
|
-
exit_code: exit_code,
|
|
36
|
-
service: service_name
|
|
37
|
-
}
|
|
19
|
+
if platform_token.nil? || platform_token.strip.empty?
|
|
20
|
+
return { success: false, error: "platform_token is required" }
|
|
38
21
|
end
|
|
39
22
|
|
|
40
|
-
puts "
|
|
41
|
-
puts "
|
|
42
|
-
|
|
43
|
-
# Monitor deployment status
|
|
44
|
-
result = monitor_deployment(service_name)
|
|
45
|
-
result[:service] = service_name
|
|
46
|
-
result
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Monitor deployment status until completion or timeout
|
|
50
|
-
#
|
|
51
|
-
# @param service_name [String] Service name
|
|
52
|
-
# @return [Hash] Deployment result
|
|
53
|
-
def self.monitor_deployment(service_name)
|
|
54
|
-
start_time = Time.now
|
|
55
|
-
last_status = nil
|
|
23
|
+
puts "🚀 Deploying service: #{service_name}"
|
|
24
|
+
puts " (This may take several minutes - you'll see live build logs below)"
|
|
25
|
+
puts ""
|
|
56
26
|
|
|
57
|
-
|
|
58
|
-
|
|
27
|
+
env = { "RAILWAY_TOKEN" => platform_token }
|
|
28
|
+
|
|
29
|
+
# Use railway up without --detach to block and show live logs
|
|
30
|
+
success = system(
|
|
31
|
+
env,
|
|
32
|
+
"railway", "up", "--service", service_name,
|
|
33
|
+
in: :close # Don't redirect stdout/stderr - let user see live logs
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if success
|
|
37
|
+
puts ""
|
|
38
|
+
puts "✅ Deployment completed successfully"
|
|
59
39
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
success: false,
|
|
63
|
-
error: "Deployment timeout",
|
|
64
|
-
details: "Deployment exceeded maximum wait time of #{MAX_WAIT_TIME} seconds",
|
|
65
|
-
elapsed: elapsed
|
|
66
|
-
}
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Get deployment status
|
|
70
|
-
status = get_deployment_status(service_name)
|
|
40
|
+
# Extract URL from railway status after successful deployment
|
|
41
|
+
url = extract_url(env, service_name)
|
|
71
42
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
# Check if deployment completed
|
|
79
|
-
if status[:completed]
|
|
80
|
-
if status[:success]
|
|
81
|
-
return {
|
|
82
|
-
success: true,
|
|
83
|
-
message: "Deployment completed successfully",
|
|
84
|
-
elapsed: elapsed,
|
|
85
|
-
final_status: status[:current_status]
|
|
86
|
-
}
|
|
87
|
-
else
|
|
88
|
-
return {
|
|
89
|
-
success: false,
|
|
90
|
-
error: "Deployment failed",
|
|
91
|
-
details: status[:error_message],
|
|
92
|
-
elapsed: elapsed,
|
|
93
|
-
final_status: status[:current_status]
|
|
94
|
-
}
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# Wait before next poll
|
|
99
|
-
sleep POLL_INTERVAL
|
|
43
|
+
return { success: true, url: url }
|
|
44
|
+
else
|
|
45
|
+
puts ""
|
|
46
|
+
puts "❌ Deployment failed"
|
|
47
|
+
return { success: false, error: "railway up exited with error code" }
|
|
100
48
|
end
|
|
49
|
+
|
|
50
|
+
rescue => e
|
|
51
|
+
{ success: false, error: "Unexpected error: #{e.message}" }
|
|
101
52
|
end
|
|
102
53
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
# @return [Hash] Status information
|
|
107
|
-
def self.get_deployment_status(service_name)
|
|
108
|
-
command = "clackycli service list --json"
|
|
109
|
-
output = `#{command} 2>&1`
|
|
54
|
+
private_class_method def self.extract_url(env, service_name)
|
|
55
|
+
require "json"
|
|
56
|
+
require "tempfile"
|
|
110
57
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
58
|
+
Tempfile.create("railway_status") do |tmpfile|
|
|
59
|
+
system(
|
|
60
|
+
env,
|
|
61
|
+
"railway", "status", "--json",
|
|
62
|
+
in: :close,
|
|
63
|
+
out: tmpfile,
|
|
64
|
+
err: File::NULL
|
|
65
|
+
)
|
|
114
66
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
success: false,
|
|
119
|
-
error_message: "Service not found: #{service_name}"
|
|
120
|
-
}
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
# Get latest deployment
|
|
124
|
-
deployments = service['deployments'] || []
|
|
125
|
-
latest = deployments.first
|
|
67
|
+
tmpfile.rewind
|
|
68
|
+
output = tmpfile.read.strip
|
|
69
|
+
return nil if output.empty?
|
|
126
70
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
status = latest['status']
|
|
71
|
+
status_data = JSON.parse(output)
|
|
72
|
+
|
|
73
|
+
# Navigate: environments.edges[].node (name="production").serviceInstances.edges[].node
|
|
74
|
+
env_edges = status_data.dig("environments", "edges")
|
|
75
|
+
return nil unless env_edges.is_a?(Array)
|
|
135
76
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
{
|
|
145
|
-
completed: true,
|
|
146
|
-
success: false,
|
|
147
|
-
current_status: status,
|
|
148
|
-
error_message: latest['error'] || 'Deployment failed'
|
|
149
|
-
}
|
|
150
|
-
else
|
|
151
|
-
{
|
|
152
|
-
completed: false,
|
|
153
|
-
current_status: status
|
|
154
|
-
}
|
|
77
|
+
prod_env = env_edges.find { |e| e.dig("node", "name") == "production" }
|
|
78
|
+
return nil unless prod_env
|
|
79
|
+
|
|
80
|
+
service_edges = prod_env.dig("node", "serviceInstances", "edges")
|
|
81
|
+
return nil unless service_edges.is_a?(Array)
|
|
82
|
+
|
|
83
|
+
service_edge = service_edges.find do |edge|
|
|
84
|
+
edge.dig("node", "serviceName") == service_name
|
|
155
85
|
end
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
86
|
+
return nil unless service_edge
|
|
87
|
+
|
|
88
|
+
domains = service_edge.dig("node", "domains", "customDomains")
|
|
89
|
+
return nil unless domains.is_a?(Array) && !domains.empty?
|
|
90
|
+
|
|
91
|
+
domain = domains.first["domain"]
|
|
92
|
+
domain ? "https://#{domain}" : nil
|
|
162
93
|
end
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
# Escape shell arguments
|
|
166
|
-
#
|
|
167
|
-
# @param str [String] String to escape
|
|
168
|
-
# @return [String] Escaped string
|
|
169
|
-
def self.shell_escape(str)
|
|
170
|
-
"'#{str.gsub("'", "'\\\\''")}'"
|
|
94
|
+
rescue
|
|
95
|
+
nil
|
|
171
96
|
end
|
|
172
97
|
end
|
|
173
98
|
end
|