openclacky 0.7.0 → 0.7.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/.clacky/skills/commit/SKILL.md +29 -4
- data/.clackyrules +3 -1
- data/CHANGELOG.md +103 -2
- data/README.md +70 -161
- data/bin/clarky +11 -0
- data/docs/HOW-TO-USE-CN.md +96 -0
- data/docs/HOW-TO-USE.md +94 -0
- data/docs/config.example.yml +27 -0
- data/docs/deploy_subagent_design.md +540 -0
- data/docs/time_machine_design.md +247 -0
- data/docs/why-openclacky.md +0 -1
- data/lib/clacky/agent/cost_tracker.rb +180 -0
- data/lib/clacky/agent/llm_caller.rb +54 -0
- data/lib/clacky/{message_compressor.rb → agent/message_compressor.rb} +12 -36
- data/lib/clacky/agent/message_compressor_helper.rb +534 -0
- data/lib/clacky/agent/session_serializer.rb +152 -0
- data/lib/clacky/agent/skill_manager.rb +138 -0
- data/lib/clacky/agent/system_prompt_builder.rb +96 -0
- data/lib/clacky/agent/time_machine.rb +199 -0
- data/lib/clacky/agent/tool_executor.rb +434 -0
- data/lib/clacky/{tool_registry.rb → agent/tool_registry.rb} +1 -1
- data/lib/clacky/agent.rb +260 -1370
- data/lib/clacky/agent_config.rb +447 -10
- data/lib/clacky/cli.rb +275 -98
- data/lib/clacky/client.rb +12 -2
- data/lib/clacky/default_skills/code-explorer/SKILL.md +34 -0
- data/lib/clacky/default_skills/deploy/SKILL.md +13 -0
- data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +383 -0
- data/lib/clacky/default_skills/deploy/tools/check_health.rb +116 -0
- data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +174 -0
- data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +67 -0
- data/lib/clacky/default_skills/deploy/tools/list_services.rb +80 -0
- data/lib/clacky/default_skills/deploy/tools/report_deploy_status.rb +67 -0
- data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +138 -0
- data/lib/clacky/default_skills/new/SKILL.md +2 -2
- data/lib/clacky/json_ui_controller.rb +195 -0
- data/lib/clacky/providers.rb +107 -0
- data/lib/clacky/skill.rb +48 -7
- data/lib/clacky/skill_loader.rb +7 -0
- data/lib/clacky/tools/edit.rb +105 -48
- data/lib/clacky/tools/file_reader.rb +44 -73
- data/lib/clacky/tools/invoke_skill.rb +89 -0
- data/lib/clacky/tools/list_tasks.rb +54 -0
- data/lib/clacky/tools/redo_task.rb +41 -0
- data/lib/clacky/tools/safe_shell.rb +1 -1
- data/lib/clacky/tools/shell.rb +74 -62
- data/lib/clacky/tools/trash_manager.rb +1 -1
- data/lib/clacky/tools/undo_task.rb +32 -0
- data/lib/clacky/tools/web_fetch.rb +2 -1
- data/lib/clacky/ui2/components/command_suggestions.rb +13 -3
- data/lib/clacky/ui2/components/inline_input.rb +23 -2
- data/lib/clacky/ui2/components/input_area.rb +65 -21
- data/lib/clacky/ui2/components/modal_component.rb +199 -62
- data/lib/clacky/ui2/layout_manager.rb +75 -25
- data/lib/clacky/ui2/line_editor.rb +23 -2
- data/lib/clacky/ui2/markdown_renderer.rb +31 -10
- data/lib/clacky/ui2/screen_buffer.rb +2 -0
- data/lib/clacky/ui2/ui_controller.rb +316 -37
- data/lib/clacky/ui2.rb +2 -0
- data/lib/clacky/ui_interface.rb +50 -0
- data/lib/clacky/utils/arguments_parser.rb +31 -3
- data/lib/clacky/utils/file_processor.rb +13 -18
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky.rb +19 -9
- data/scripts/install.sh +274 -97
- data/scripts/uninstall.sh +12 -12
- metadata +40 -13
- data/.clacky/skills/test-skill/SKILL.md +0 -15
- data/lib/clacky/compression/base.rb +0 -231
- data/lib/clacky/compression/standard.rb +0 -339
- data/lib/clacky/config.rb +0 -117
- /data/lib/clacky/{hook_manager.rb → agent/hook_manager.rb} +0 -0
- /data/lib/clacky/{progress_indicator.rb → ui2/progress_indicator.rb} +0 -0
- /data/lib/clacky/{thinking_verbs.rb → ui2/thinking_verbs.rb} +0 -0
- /data/lib/clacky/{gitignore_parser.rb → utils/gitignore_parser.rb} +0 -0
- /data/lib/clacky/{model_pricing.rb → utils/model_pricing.rb} +0 -0
- /data/lib/clacky/{trash_directory.rb → utils/trash_directory.rb} +0 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../tools/list_services'
|
|
4
|
+
require_relative '../tools/report_deploy_status'
|
|
5
|
+
require_relative '../tools/execute_deployment'
|
|
6
|
+
require_relative '../tools/set_deploy_variables'
|
|
7
|
+
require_relative '../tools/fetch_runtime_logs'
|
|
8
|
+
require_relative '../tools/check_health'
|
|
9
|
+
|
|
10
|
+
module Clacky
|
|
11
|
+
module DeployTemplates
|
|
12
|
+
# Rails deployment template - Fixed 8-step deployment process
|
|
13
|
+
# No AI decision-making, pure automation
|
|
14
|
+
class RailsDeploy
|
|
15
|
+
# Execute the Rails deployment workflow
|
|
16
|
+
#
|
|
17
|
+
# @return [Hash] Deployment result
|
|
18
|
+
def self.execute
|
|
19
|
+
# CRITICAL: Check environment requirements
|
|
20
|
+
unless environment_valid?
|
|
21
|
+
return {
|
|
22
|
+
success: false,
|
|
23
|
+
error: "Railway deployment is only available in the Clacky cloud environment with Rails template projects.",
|
|
24
|
+
details: environment_check_details
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
puts "\n" + "="*60
|
|
29
|
+
puts "🚂 Rails Deployment Template (8-Step Process)"
|
|
30
|
+
puts "="*60 + "\n"
|
|
31
|
+
|
|
32
|
+
# Step 1: List services
|
|
33
|
+
step1_result = step1_list_services
|
|
34
|
+
return step1_result unless step1_result[:success]
|
|
35
|
+
|
|
36
|
+
services = step1_result[:services]
|
|
37
|
+
main_service = step1_result[:main_service]
|
|
38
|
+
db_service = step1_result[:db_service]
|
|
39
|
+
|
|
40
|
+
# Step 2: Check first deployment
|
|
41
|
+
step2_result = step2_check_first_deployment(main_service)
|
|
42
|
+
return step2_result unless step2_result[:success]
|
|
43
|
+
|
|
44
|
+
is_first = step2_result[:is_first_deployment]
|
|
45
|
+
|
|
46
|
+
# Step 3: Set Rails environment variables
|
|
47
|
+
step3_result = step3_set_rails_variables(main_service, db_service, is_first)
|
|
48
|
+
return step3_result unless step3_result[:success]
|
|
49
|
+
|
|
50
|
+
# Step 4: Execute deployment
|
|
51
|
+
step4_result = step4_execute_deployment(main_service)
|
|
52
|
+
return step4_result unless step4_result[:success]
|
|
53
|
+
|
|
54
|
+
# Step 5: Run database migrations
|
|
55
|
+
step5_result = step5_run_migrations(main_service)
|
|
56
|
+
return step5_result unless step5_result[:success]
|
|
57
|
+
|
|
58
|
+
# Step 6: Run database seeds (first deployment only)
|
|
59
|
+
if is_first
|
|
60
|
+
step6_result = step6_run_seeds(main_service)
|
|
61
|
+
return step6_result unless step6_result[:success]
|
|
62
|
+
else
|
|
63
|
+
puts "\n[Step 6] Skipping database seeds (not first deployment)\n"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Step 7: Health check
|
|
67
|
+
step7_result = step7_health_check(main_service)
|
|
68
|
+
return step7_result unless step7_result[:success]
|
|
69
|
+
|
|
70
|
+
# Step 8: Report success
|
|
71
|
+
step8_report_success(main_service)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Check if environment is valid for deployment
|
|
75
|
+
#
|
|
76
|
+
# @return [Boolean] true if both required environment variables are "true"
|
|
77
|
+
def self.environment_valid?
|
|
78
|
+
ENV['IS_RAILS_TEMPLATE'] == 'true' && ENV['IS_CLACKY_CDE'] == 'true'
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Get environment check details for error reporting
|
|
82
|
+
#
|
|
83
|
+
# @return [Hash] Details about environment check
|
|
84
|
+
def self.environment_check_details
|
|
85
|
+
{
|
|
86
|
+
is_rails_template: ENV['IS_RAILS_TEMPLATE'],
|
|
87
|
+
is_clacky_cde: ENV['IS_CLACKY_CDE'],
|
|
88
|
+
required: {
|
|
89
|
+
is_rails_template: 'true',
|
|
90
|
+
is_clacky_cde: 'true'
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Step 1: List Railway services
|
|
96
|
+
def self.step1_list_services
|
|
97
|
+
puts "\n[Step 1] Listing Railway services..."
|
|
98
|
+
DeployTools::ReportDeployStatus.execute(
|
|
99
|
+
status: 'analyzing',
|
|
100
|
+
message: 'Listing Railway services'
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
result = DeployTools::ListServices.execute
|
|
104
|
+
|
|
105
|
+
unless result[:success]
|
|
106
|
+
return {
|
|
107
|
+
success: false,
|
|
108
|
+
error: "Failed to list services",
|
|
109
|
+
details: result
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
services = result[:services]
|
|
114
|
+
|
|
115
|
+
# Find main web service
|
|
116
|
+
main_service = services.find { |s| s['type'] == 'web' || s['type'] == 'service' }
|
|
117
|
+
|
|
118
|
+
if main_service.nil?
|
|
119
|
+
return {
|
|
120
|
+
success: false,
|
|
121
|
+
error: "No web service found",
|
|
122
|
+
details: "Please create a web service in Railway first"
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Find database service
|
|
127
|
+
db_service = services.find { |s| s['type'] == 'postgres' || s['type'] == 'mysql' }
|
|
128
|
+
|
|
129
|
+
puts "✅ Found #{services.length} service(s)"
|
|
130
|
+
puts " Main service: #{main_service['name']}"
|
|
131
|
+
puts " Database: #{db_service ? db_service['name'] : 'None'}"
|
|
132
|
+
|
|
133
|
+
{
|
|
134
|
+
success: true,
|
|
135
|
+
services: services,
|
|
136
|
+
main_service: main_service,
|
|
137
|
+
db_service: db_service
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Step 2: Check if this is first deployment
|
|
142
|
+
def self.step2_check_first_deployment(main_service)
|
|
143
|
+
puts "\n[Step 2] Checking deployment history..."
|
|
144
|
+
|
|
145
|
+
deployments = main_service['deployments'] || []
|
|
146
|
+
is_first = deployments.empty?
|
|
147
|
+
|
|
148
|
+
if is_first
|
|
149
|
+
puts "📦 This is the FIRST deployment"
|
|
150
|
+
else
|
|
151
|
+
puts "📦 This is a SUBSEQUENT deployment (#{deployments.length} previous deployment(s))"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
{
|
|
155
|
+
success: true,
|
|
156
|
+
is_first_deployment: is_first,
|
|
157
|
+
previous_deployments: deployments.length
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Step 3: Set Rails environment variables
|
|
162
|
+
def self.step3_set_rails_variables(main_service, db_service, is_first)
|
|
163
|
+
puts "\n[Step 3] Setting Rails environment variables..."
|
|
164
|
+
DeployTools::ReportDeployStatus.execute(
|
|
165
|
+
status: 'analyzing',
|
|
166
|
+
message: 'Configuring Rails environment variables'
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
service_name = main_service['name']
|
|
170
|
+
|
|
171
|
+
# Build simple variables
|
|
172
|
+
variables = {
|
|
173
|
+
'RAILS_ENV' => 'production',
|
|
174
|
+
'RAILS_SERVE_STATIC_FILES' => 'true',
|
|
175
|
+
'RAILS_LOG_TO_STDOUT' => 'true'
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# Get or prompt for SECRET_KEY_BASE
|
|
179
|
+
secret_key_base = ENV['SECRET_KEY_BASE']
|
|
180
|
+
if secret_key_base.nil? || secret_key_base.empty?
|
|
181
|
+
puts "⚠️ SECRET_KEY_BASE not found. Please generate one:"
|
|
182
|
+
puts " Run: rails secret"
|
|
183
|
+
print "Enter SECRET_KEY_BASE: "
|
|
184
|
+
secret_key_base = $stdin.gets.chomp
|
|
185
|
+
end
|
|
186
|
+
variables['SECRET_KEY_BASE'] = secret_key_base
|
|
187
|
+
|
|
188
|
+
# Build reference variables
|
|
189
|
+
ref_variables = {}
|
|
190
|
+
if db_service
|
|
191
|
+
ref_variables['DATABASE_URL'] = "#{db_service['name']}.DATABASE_URL"
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Set variables
|
|
195
|
+
result = DeployTools::SetDeployVariables.execute(
|
|
196
|
+
service_name: service_name,
|
|
197
|
+
variables: variables,
|
|
198
|
+
ref_variables: ref_variables
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
unless result[:success]
|
|
202
|
+
return {
|
|
203
|
+
success: false,
|
|
204
|
+
error: "Failed to set environment variables",
|
|
205
|
+
details: result
|
|
206
|
+
}
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
puts "✅ Set #{result[:set_variables].length} variable(s)"
|
|
210
|
+
if result[:skipped_variables].any?
|
|
211
|
+
puts "⚠️ Skipped #{result[:skipped_variables].length} protected variable(s)"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
{
|
|
215
|
+
success: true,
|
|
216
|
+
set_count: result[:set_variables].length
|
|
217
|
+
}
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Step 4: Execute deployment
|
|
221
|
+
def self.step4_execute_deployment(main_service)
|
|
222
|
+
puts "\n[Step 4] Executing deployment..."
|
|
223
|
+
DeployTools::ReportDeployStatus.execute(
|
|
224
|
+
status: 'deploying',
|
|
225
|
+
message: 'Starting deployment to Railway'
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
service_name = main_service['name']
|
|
229
|
+
result = DeployTools::ExecuteDeployment.execute(service_name: service_name)
|
|
230
|
+
|
|
231
|
+
unless result[:success]
|
|
232
|
+
# Fetch logs for debugging
|
|
233
|
+
puts "\n❌ Deployment failed. Fetching logs..."
|
|
234
|
+
log_result = DeployTools::FetchRuntimeLogs.execute(
|
|
235
|
+
service_name: service_name,
|
|
236
|
+
lines: 100
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if log_result[:success]
|
|
240
|
+
puts "\n📋 Last 100 lines of logs:"
|
|
241
|
+
puts log_result[:logs]
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
success: false,
|
|
246
|
+
error: "Deployment failed",
|
|
247
|
+
details: result
|
|
248
|
+
}
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
puts "✅ Deployment completed in #{result[:elapsed].round(1)}s"
|
|
252
|
+
|
|
253
|
+
{
|
|
254
|
+
success: true,
|
|
255
|
+
elapsed: result[:elapsed]
|
|
256
|
+
}
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Step 5: Run database migrations
|
|
260
|
+
def self.step5_run_migrations(main_service)
|
|
261
|
+
puts "\n[Step 5] Running database migrations..."
|
|
262
|
+
|
|
263
|
+
service_name = main_service['name']
|
|
264
|
+
command = "clackycli run -s '#{service_name}' rake db:migrate"
|
|
265
|
+
|
|
266
|
+
puts "Running: rake db:migrate"
|
|
267
|
+
output = `#{command} 2>&1`
|
|
268
|
+
exit_code = $?.exitstatus
|
|
269
|
+
|
|
270
|
+
if exit_code != 0
|
|
271
|
+
return {
|
|
272
|
+
success: false,
|
|
273
|
+
error: "Database migration failed",
|
|
274
|
+
details: output
|
|
275
|
+
}
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
puts "✅ Database migrations completed"
|
|
279
|
+
puts output if output && !output.empty?
|
|
280
|
+
|
|
281
|
+
{
|
|
282
|
+
success: true
|
|
283
|
+
}
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Step 6: Run database seeds
|
|
287
|
+
def self.step6_run_seeds(main_service)
|
|
288
|
+
puts "\n[Step 6] Running database seeds..."
|
|
289
|
+
|
|
290
|
+
service_name = main_service['name']
|
|
291
|
+
command = "clackycli run -s '#{service_name}' rake db:seed"
|
|
292
|
+
|
|
293
|
+
puts "Running: rake db:seed"
|
|
294
|
+
output = `#{command} 2>&1`
|
|
295
|
+
exit_code = $?.exitstatus
|
|
296
|
+
|
|
297
|
+
if exit_code != 0
|
|
298
|
+
puts "⚠️ Warning: Database seeding failed (this may be expected)"
|
|
299
|
+
puts output if output && !output.empty?
|
|
300
|
+
else
|
|
301
|
+
puts "✅ Database seeding completed"
|
|
302
|
+
puts output if output && !output.empty?
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Don't fail deployment if seeding fails
|
|
306
|
+
{
|
|
307
|
+
success: true
|
|
308
|
+
}
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Step 7: Health check
|
|
312
|
+
def self.step7_health_check(main_service)
|
|
313
|
+
puts "\n[Step 7] Performing health check..."
|
|
314
|
+
DeployTools::ReportDeployStatus.execute(
|
|
315
|
+
status: 'checking',
|
|
316
|
+
message: 'Verifying application health'
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
public_url = main_service['public_url']
|
|
320
|
+
|
|
321
|
+
if public_url.nil? || public_url.empty?
|
|
322
|
+
puts "⚠️ Warning: No public URL found. Skipping health check."
|
|
323
|
+
return { success: true }
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Wait a bit for service to be fully ready
|
|
327
|
+
puts "⏳ Waiting 10 seconds for service to be ready..."
|
|
328
|
+
sleep 10
|
|
329
|
+
|
|
330
|
+
result = DeployTools::CheckHealth.execute(
|
|
331
|
+
url: public_url,
|
|
332
|
+
path: '/',
|
|
333
|
+
timeout: 30
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
unless result[:success]
|
|
337
|
+
puts "⚠️ Warning: Health check failed (service may still be starting)"
|
|
338
|
+
puts " Error: #{result[:error]}"
|
|
339
|
+
puts " You can manually check: #{public_url}"
|
|
340
|
+
# Don't fail deployment on health check failure
|
|
341
|
+
else
|
|
342
|
+
puts "✅ Health check passed (#{result[:status_code]} - #{result[:elapsed]}s)"
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
{
|
|
346
|
+
success: true,
|
|
347
|
+
health_check_result: result
|
|
348
|
+
}
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Step 8: Report success
|
|
352
|
+
def self.step8_report_success(main_service)
|
|
353
|
+
puts "\n[Step 8] Deployment completed successfully! 🎉"
|
|
354
|
+
|
|
355
|
+
public_url = main_service['public_url']
|
|
356
|
+
|
|
357
|
+
DeployTools::ReportDeployStatus.execute(
|
|
358
|
+
status: 'success',
|
|
359
|
+
message: "Rails app deployed successfully"
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
puts "\n" + "="*60
|
|
363
|
+
puts "✅ DEPLOYMENT SUCCESSFUL"
|
|
364
|
+
puts "="*60
|
|
365
|
+
puts "Service: #{main_service['name']}"
|
|
366
|
+
puts "URL: #{public_url || 'Not available yet'}"
|
|
367
|
+
puts "="*60 + "\n"
|
|
368
|
+
|
|
369
|
+
{
|
|
370
|
+
success: true,
|
|
371
|
+
service: main_service['name'],
|
|
372
|
+
url: public_url
|
|
373
|
+
}
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Run deployment if executed directly
|
|
380
|
+
if __FILE__ == $0
|
|
381
|
+
result = Clacky::DeployTemplates::RailsDeploy.execute
|
|
382
|
+
exit(result[:success] ? 0 : 1)
|
|
383
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
module Clacky
|
|
8
|
+
module DeployTools
|
|
9
|
+
# Perform HTTP health check on deployed application
|
|
10
|
+
class CheckHealth
|
|
11
|
+
DEFAULT_PATH = '/'
|
|
12
|
+
DEFAULT_TIMEOUT = 30
|
|
13
|
+
MAX_TIMEOUT = 120
|
|
14
|
+
|
|
15
|
+
# Perform health check
|
|
16
|
+
#
|
|
17
|
+
# @param url [String] Optional URL (defaults to RAILWAY_PUBLIC_DOMAIN env var)
|
|
18
|
+
# @param path [String] Health check path (default: "/")
|
|
19
|
+
# @param timeout [Integer] Request timeout in seconds (default: 30)
|
|
20
|
+
# @return [Hash] Result of health check
|
|
21
|
+
def self.execute(url: nil, path: DEFAULT_PATH, timeout: DEFAULT_TIMEOUT)
|
|
22
|
+
# Get URL from parameter or environment
|
|
23
|
+
target_url = url || ENV['RAILWAY_PUBLIC_DOMAIN']
|
|
24
|
+
|
|
25
|
+
if target_url.nil? || target_url.empty?
|
|
26
|
+
return {
|
|
27
|
+
error: "No URL provided",
|
|
28
|
+
details: "Please provide a URL or set RAILWAY_PUBLIC_DOMAIN environment variable"
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Ensure URL has protocol
|
|
33
|
+
target_url = "https://#{target_url}" unless target_url.start_with?('http://', 'https://')
|
|
34
|
+
|
|
35
|
+
# Build full URL with path
|
|
36
|
+
full_url = "#{target_url.chomp('/')}#{path}"
|
|
37
|
+
|
|
38
|
+
# Validate timeout
|
|
39
|
+
timeout = timeout.to_i
|
|
40
|
+
if timeout <= 0 || timeout > MAX_TIMEOUT
|
|
41
|
+
timeout = DEFAULT_TIMEOUT
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
puts "🏥 Checking health: #{full_url} (timeout: #{timeout}s)"
|
|
45
|
+
|
|
46
|
+
begin
|
|
47
|
+
uri = URI.parse(full_url)
|
|
48
|
+
result = perform_request(uri, timeout)
|
|
49
|
+
|
|
50
|
+
if result[:success]
|
|
51
|
+
puts "✅ Health check passed: #{result[:status_code]}"
|
|
52
|
+
else
|
|
53
|
+
puts "❌ Health check failed: #{result[:error]}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
result.merge(url: full_url, path: path)
|
|
57
|
+
rescue URI::InvalidURIError => e
|
|
58
|
+
{
|
|
59
|
+
success: false,
|
|
60
|
+
error: "Invalid URL",
|
|
61
|
+
details: e.message,
|
|
62
|
+
url: full_url
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Perform HTTP request
|
|
68
|
+
#
|
|
69
|
+
# @param uri [URI] Target URI
|
|
70
|
+
# @param timeout [Integer] Timeout in seconds
|
|
71
|
+
# @return [Hash] Request result
|
|
72
|
+
def self.perform_request(uri, timeout)
|
|
73
|
+
start_time = Time.now
|
|
74
|
+
|
|
75
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
76
|
+
http.use_ssl = (uri.scheme == 'https')
|
|
77
|
+
http.open_timeout = timeout
|
|
78
|
+
http.read_timeout = timeout
|
|
79
|
+
|
|
80
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
|
81
|
+
request['User-Agent'] = 'Clacky-Deploy-Health-Check/1.0'
|
|
82
|
+
|
|
83
|
+
response = http.request(request)
|
|
84
|
+
elapsed = Time.now - start_time
|
|
85
|
+
|
|
86
|
+
{
|
|
87
|
+
success: response.is_a?(Net::HTTPSuccess),
|
|
88
|
+
status_code: response.code.to_i,
|
|
89
|
+
status_message: response.message,
|
|
90
|
+
elapsed: elapsed.round(2),
|
|
91
|
+
headers: response.to_hash,
|
|
92
|
+
body_preview: response.body&.slice(0, 500) # First 500 chars
|
|
93
|
+
}
|
|
94
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
95
|
+
{
|
|
96
|
+
success: false,
|
|
97
|
+
error: "Request timeout",
|
|
98
|
+
details: e.message,
|
|
99
|
+
elapsed: timeout
|
|
100
|
+
}
|
|
101
|
+
rescue SocketError => e
|
|
102
|
+
{
|
|
103
|
+
success: false,
|
|
104
|
+
error: "Network error",
|
|
105
|
+
details: e.message
|
|
106
|
+
}
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
{
|
|
109
|
+
success: false,
|
|
110
|
+
error: "Health check failed",
|
|
111
|
+
details: "#{e.class}: #{e.message}"
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Clacky
|
|
6
|
+
module DeployTools
|
|
7
|
+
# Execute deployment and monitor until completion
|
|
8
|
+
class ExecuteDeployment
|
|
9
|
+
MAX_WAIT_TIME = 600 # 10 minutes
|
|
10
|
+
POLL_INTERVAL = 5 # 5 seconds
|
|
11
|
+
|
|
12
|
+
# Execute deployment for a service
|
|
13
|
+
#
|
|
14
|
+
# @param service_name [String] Service to deploy
|
|
15
|
+
# @return [Hash] Result of the deployment
|
|
16
|
+
def self.execute(service_name:)
|
|
17
|
+
if service_name.nil? || service_name.empty?
|
|
18
|
+
return {
|
|
19
|
+
error: "Service name is required",
|
|
20
|
+
details: "Please provide a valid service name"
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
puts "🚀 Starting deployment for service: #{service_name}"
|
|
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
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
puts "✅ Deployment triggered successfully"
|
|
41
|
+
puts "⏳ Monitoring deployment progress..."
|
|
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
|
|
56
|
+
|
|
57
|
+
loop do
|
|
58
|
+
elapsed = Time.now - start_time
|
|
59
|
+
|
|
60
|
+
if elapsed > MAX_WAIT_TIME
|
|
61
|
+
return {
|
|
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)
|
|
71
|
+
|
|
72
|
+
# Print status update if changed
|
|
73
|
+
if status[:current_status] != last_status
|
|
74
|
+
puts "📊 Status: #{status[:current_status]}"
|
|
75
|
+
last_status = status[:current_status]
|
|
76
|
+
end
|
|
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
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Get current deployment status
|
|
104
|
+
#
|
|
105
|
+
# @param service_name [String] Service name
|
|
106
|
+
# @return [Hash] Status information
|
|
107
|
+
def self.get_deployment_status(service_name)
|
|
108
|
+
command = "clackycli service list --json"
|
|
109
|
+
output = `#{command} 2>&1`
|
|
110
|
+
|
|
111
|
+
begin
|
|
112
|
+
services = JSON.parse(output)
|
|
113
|
+
service = services.find { |s| s['name'] == service_name }
|
|
114
|
+
|
|
115
|
+
if service.nil?
|
|
116
|
+
return {
|
|
117
|
+
completed: true,
|
|
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
|
|
126
|
+
|
|
127
|
+
if latest.nil?
|
|
128
|
+
return {
|
|
129
|
+
completed: false,
|
|
130
|
+
current_status: 'waiting'
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
status = latest['status']
|
|
135
|
+
|
|
136
|
+
case status
|
|
137
|
+
when 'SUCCESS', 'ACTIVE'
|
|
138
|
+
{
|
|
139
|
+
completed: true,
|
|
140
|
+
success: true,
|
|
141
|
+
current_status: status
|
|
142
|
+
}
|
|
143
|
+
when 'FAILED', 'CRASHED'
|
|
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
|
+
}
|
|
155
|
+
end
|
|
156
|
+
rescue JSON::ParserError
|
|
157
|
+
{
|
|
158
|
+
completed: true,
|
|
159
|
+
success: false,
|
|
160
|
+
error_message: "Failed to parse deployment status"
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
end
|
|
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("'", "'\\\\''")}'"
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|