kdeploy 0.2.0 → 0.4.0

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.
data/lib/kdeploy/cli.rb CHANGED
@@ -1,1452 +1,236 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Add String truncate method if not available
4
- class String
5
- def truncate(length)
6
- return self if size <= length
7
-
8
- "#{self[0, length - 3]}..."
9
- end
10
- end
3
+ require 'thor'
4
+ require 'pastel'
5
+ require 'tty-table'
6
+ require 'tty-box'
7
+ require 'fileutils'
11
8
 
12
9
  module Kdeploy
13
- # Command Line Interface for kdeploy
14
10
  class CLI < Thor
15
- # Fix Thor deprecation warning
11
+ extend DSL
12
+
16
13
  def self.exit_on_failure?
17
14
  true
18
15
  end
19
16
 
20
- # Common options for commands that execute scripts
21
- SCRIPT_OPTIONS = {
22
- config: { aliases: '-c', desc: 'Configuration file path' },
23
- inventory: { aliases: '-i', desc: 'Inventory file path' },
24
- dry_run: { aliases: '-d', type: :boolean, desc: 'Perform dry run without executing' },
25
- verbose: { aliases: '-v', type: :boolean, desc: 'Enable verbose output' },
26
- log_file: { aliases: '-l', desc: 'Log file path' }
27
- }.freeze
28
-
29
- desc 'execute SCRIPT', 'Execute deployment script'
30
- SCRIPT_OPTIONS.each { |name, opts| option(name, opts) }
31
- def execute(script_file)
32
- setup_configuration
33
- setup_logging
34
-
35
- validate_script_file(script_file)
36
-
37
- begin
38
- options[:dry_run] ? perform_dry_run(script_file) : execute_script(script_file)
39
- rescue Kdeploy::Error => e
40
- handle_deployment_error(e)
41
- rescue StandardError => e
42
- handle_unexpected_error(e)
43
- end
44
- end
45
-
46
- desc 'deploy SCRIPT', 'Execute deployment script (alias for execute)'
47
- SCRIPT_OPTIONS.each { |name, opts| option(name, opts) }
48
- def deploy(script_file)
49
- execute(script_file)
50
- end
51
-
52
- desc 'init [PROJECT_NAME]', 'Initialize new deployment project'
53
- option :name, aliases: '-n', desc: 'Specify project name'
54
- def init(project_name = nil)
55
- show_kdeploy_banner
56
- project_name = determine_project_name(project_name)
57
-
58
- display_init_header(project_name)
59
- create_project_structure(project_name)
60
- display_init_success(project_name)
61
- end
62
-
63
- desc 'validate SCRIPT', 'Validate deployment script'
64
- option :config, aliases: '-c', desc: 'Configuration file path'
65
- option :inventory, aliases: '-i', desc: 'Inventory file path'
66
- def validate(script_file)
67
- show_kdeploy_banner
68
- setup_configuration
69
-
70
- display_validation_header(script_file)
71
- validate_script_file(script_file)
72
-
73
- begin
74
- pipeline = Kdeploy.load_script(script_file)
75
- validation_errors = pipeline.validate
76
-
77
- if validation_errors.empty?
78
- display_validation_success(pipeline)
79
- else
80
- display_validation_errors(validation_errors)
81
- end
82
- rescue Kdeploy::Error => e
83
- display_validation_failure(e)
84
- end
85
- end
86
-
87
- desc 'config', 'Show configuration'
88
- option :config, aliases: '-c', desc: 'Configuration file path'
89
- option :inventory, aliases: '-i', desc: 'Inventory file path'
90
- option :verbose, aliases: '-v', type: :boolean, desc: 'Enable verbose output'
91
- option :log_file, aliases: '-l', desc: 'Log file path'
92
- def config
93
- show_kdeploy_banner
94
- setup_configuration
95
-
96
- display_configuration(Kdeploy.configuration)
97
- end
17
+ map %w[--help -h] => :help
18
+ map %w[--version -v] => :version
98
19
 
99
- desc 'version', 'Show version'
20
+ desc 'version', 'Show version information'
100
21
  def version
101
- show_kdeploy_banner
102
- display_version_info
22
+ puts Kdeploy::Banner.show_version
103
23
  end
104
24
 
105
- desc 'help [COMMAND]', 'Describe available commands or one specific command'
25
+ desc 'help [COMMAND]', 'Show help information'
106
26
  def help(command = nil)
107
- show_kdeploy_banner
108
- puts ''
109
- puts '📖 Available Commands:'.colorize(:yellow)
110
- puts ''
111
-
112
27
  if command
113
28
  super
114
29
  else
115
- display_available_commands
116
- end
117
- end
30
+ pastel = Pastel.new
31
+ puts Kdeploy::Banner.show
32
+ puts <<~HELP
33
+ #{pastel.bright_white('📖 Available Commands:')}
118
34
 
119
- desc 'stats [COMMAND]', 'Show deployment statistics'
120
- option :days, aliases: '-d', type: :numeric, default: 30, desc: 'Number of days to analyze'
121
- option :format, aliases: '-f', type: :string, default: 'text', desc: 'Output format (text, json, csv)'
122
- option :output, aliases: '-o', type: :string, desc: 'Output file path'
123
- def stats(command = 'summary')
124
- case command.downcase
125
- when 'summary'
126
- show_summary_stats
127
- when 'deployments'
128
- show_deployment_stats
129
- when 'tasks'
130
- show_task_stats
131
- when 'failures'
132
- show_failure_stats
133
- when 'trends'
134
- show_trend_stats
135
- when 'global'
136
- show_global_stats
137
- when 'clear'
138
- clear_statistics
139
- when 'export'
140
- export_statistics
141
- else
142
- error "Unknown stats command: #{command}"
143
- puts ''
144
- puts 'Available commands: summary, deployments, tasks, failures, trends, global, clear, export'
145
- exit 1
146
- end
147
- end
35
+ #{pastel.bright_yellow('🚀')} #{pastel.bright_white('execute TASK_FILE [TASK]')} Execute deployment tasks from file
36
+ #{pastel.dim(' --limit HOSTS')} Limit to specific hosts (comma-separated)
37
+ #{pastel.dim(' --parallel NUM')} Number of parallel executions (default: 5)
38
+ #{pastel.dim(' --dry-run')} Show what would be done without executing
148
39
 
149
- private
40
+ #{pastel.bright_yellow('🆕')} #{pastel.bright_white('init [DIR]')} Initialize new deployment project
41
+ #{pastel.bright_yellow('ℹ️')} #{pastel.bright_white('version')} Show version information
42
+ #{pastel.bright_yellow('❓')} #{pastel.bright_white('help [COMMAND]')} Show help information
150
43
 
151
- def display_configuration(config)
152
- puts ''
153
- puts '⚙️ Current Configuration'.colorize(:yellow)
154
- puts '=' * 50
44
+ #{pastel.bright_white('💡 Examples:')}
155
45
 
156
- display_execution_settings(config)
157
- display_network_settings(config)
158
- display_file_settings(config)
159
- display_logging_settings(config)
160
- display_ssh_options(config) if config.respond_to?(:ssh_options) && config.ssh_options&.any?
46
+ #{pastel.dim('# Initialize a new project')}
47
+ #{pastel.bright_cyan('kdeploy init my-deployment')}
161
48
 
162
- puts ''
163
- puts '💡 Use --config FILE to load custom configuration'.colorize(:yellow)
164
- puts ''
165
- end
49
+ #{pastel.dim('# Deploy to web servers')}
50
+ #{pastel.bright_cyan('kdeploy execute deploy.rb deploy_web')}
166
51
 
167
- def display_execution_settings(config)
168
- puts ''
169
- puts '🔧 Execution Settings'.colorize(:cyan)
170
- puts " Max Concurrent Tasks: #{config.max_concurrent_tasks}".colorize(:light_blue)
171
- puts " Retry Count: #{config.retry_count}".colorize(:light_blue)
172
- puts " Retry Delay: #{config.retry_delay}s".colorize(:light_blue)
173
- end
52
+ #{pastel.dim('# Backup database')}
53
+ #{pastel.bright_cyan('kdeploy execute deploy.rb backup_db')}
174
54
 
175
- def display_network_settings(config)
176
- puts ''
177
- puts '🌐 Network & SSH Settings'.colorize(:cyan)
178
- puts " SSH Timeout: #{config.ssh_timeout}s".colorize(:light_blue)
179
- puts " Command Timeout: #{config.command_timeout}s".colorize(:light_blue)
180
- puts " Default User: #{config.default_user}".colorize(:light_blue)
181
- puts " Default Port: #{config.default_port}".colorize(:light_blue)
182
- end
55
+ #{pastel.dim('# Run maintenance on specific hosts')}
56
+ #{pastel.bright_cyan('kdeploy execute deploy.rb maintenance --limit web01')}
183
57
 
184
- def display_file_settings(config)
185
- puts ''
186
- puts '📁 File & Directory Settings'.colorize(:cyan)
187
- puts " Inventory File: #{config.inventory_file || 'not specified'}".colorize(:light_blue)
188
- puts " Template Directory: #{config.template_dir}".colorize(:light_blue)
189
- end
58
+ #{pastel.dim('# Preview deployment')}
59
+ #{pastel.bright_cyan('kdeploy execute deploy.rb deploy_web --dry-run')}
190
60
 
191
- def display_logging_settings(config)
192
- puts ''
193
- puts '📋 Logging Settings'.colorize(:cyan)
194
- puts " Log Level: #{config.log_level}".colorize(:light_blue)
195
- puts " Log File: #{config.log_file || 'stdout'}".colorize(:light_blue)
196
- end
61
+ #{pastel.bright_white('📚 Documentation:')}
62
+ #{pastel.bright_cyan('https://github.com/kevin197011/kdeploy')}
197
63
 
198
- def display_ssh_options(config)
199
- puts ''
200
- puts '🔐 SSH Options'.colorize(:cyan)
201
- config.ssh_options.each do |key, value|
202
- puts " #{key.to_s.capitalize.gsub('_', ' ')}: #{value}".colorize(:light_blue)
64
+ HELP
203
65
  end
204
66
  end
205
67
 
206
- def display_version_info
207
- puts ''
208
- puts "🔖 Version: #{Kdeploy::VERSION}".colorize(:green)
209
- puts '📅 Released: 2025'.colorize(:light_blue)
210
- puts '🏠 Homepage: https://github.com/kevin197011/kdeploy'.colorize(:light_blue)
211
- puts '📚 Documentation: https://github.com/kevin197011/kdeploy/wiki'.colorize(:light_blue)
212
- puts ''
213
- end
214
-
215
- def display_available_commands
216
- puts " 🚀 #{'deploy SCRIPT'.ljust(25)} Execute deployment script"
217
- puts " ⚡ #{'execute SCRIPT'.ljust(25)} Execute deployment script (alias for deploy)"
218
- puts " 🆕 #{'init [PROJECT_NAME]'.ljust(25)} Initialize new deployment project"
219
- puts " ✅ #{'validate SCRIPT'.ljust(25)} Validate deployment script"
220
- puts " ⚙️ #{'config'.ljust(25)} Show configuration"
221
- puts " 📊 #{'stats [COMMAND]'.ljust(25)} Show deployment statistics"
222
- puts " 🔖 #{'version'.ljust(25)} Show version"
223
- puts ''
224
- puts '💡 For more details on a command:'.colorize(:yellow)
225
- puts ' kdeploy help COMMAND'.colorize(:light_blue)
226
- puts ''
227
- end
228
-
229
- def setup_configuration
230
- Kdeploy.configure do |config|
231
- config.config_file = options[:config] if options[:config]
232
- config.inventory_file = options[:inventory] if options[:inventory]
233
- config.log_level = options[:verbose] ? :debug : :info
234
- config.log_file = options[:log_file] if options[:log_file]
235
- end
236
- end
237
-
238
- def setup_logging
239
- config = Kdeploy.configuration
240
- KdeployLogger.setup(
241
- level: config.log_level,
242
- log_file: config.log_file
243
- )
244
- end
245
-
246
- def perform_dry_run(script_file)
247
- info '🔍 Performing dry run...'
248
- pipeline = Kdeploy.load_script(script_file)
249
-
250
- puts ''
251
- puts '📋 Pipeline Summary:'.colorize(:cyan)
252
- puts " Name: #{pipeline.name}".colorize(:light_blue)
253
- puts " Hosts: #{pipeline.hosts.size}".colorize(:light_blue)
254
- puts " Tasks: #{pipeline.tasks.size}".colorize(:light_blue)
255
-
256
- display_target_hosts(pipeline.hosts) if pipeline.hosts.any?
257
- display_tasks(pipeline.tasks) if pipeline.tasks.any?
258
-
259
- success '✅ Dry run completed'
260
- end
261
-
262
- def execute_script(script_file)
263
- show_kdeploy_banner
264
- info "🚀 Executing deployment script: #{script_file}"
265
-
266
- pipeline = Kdeploy.load_script(script_file)
267
- result = pipeline.execute
268
-
269
- if result[:success]
270
- success '✅ Deployment completed successfully'
271
- else
272
- error '❌ Deployment failed'
273
- exit 1
274
- end
275
- end
276
-
277
- def show_kdeploy_banner
278
- Banner.show
279
- end
280
-
281
- def info(message)
282
- puts message.colorize(:blue)
283
- end
284
-
285
- def success(message)
286
- puts message.colorize(:green)
287
- end
288
-
289
- def error(message)
290
- puts message.colorize(:red)
291
- end
292
-
293
- def create_project_structure(project_name)
294
- create_project_directories(project_name)
295
- create_sample_files(project_name)
296
- create_sample_scripts(project_name)
297
- create_sample_templates(project_name)
298
- end
299
-
300
- def create_project_directories(project_name)
301
- dirs = [
302
- project_name,
303
- "#{project_name}/config",
304
- "#{project_name}/scripts",
305
- "#{project_name}/templates"
306
- ]
307
-
308
- dirs.each do |dir|
309
- FileUtils.mkdir_p(dir)
310
- info "Created directory: #{dir}"
311
- end
312
- end
313
-
314
- def create_sample_files(project_name)
315
- create_deploy_script(project_name)
316
- create_inventory_file(project_name)
317
- create_config_file(project_name)
318
- end
319
-
320
- def create_deploy_script(project_name)
321
- content = <<~RUBY
322
- # frozen_string_literal: true
323
-
324
- # Main deployment script
325
- pipeline 'main' do
326
- # Define target hosts
327
- host 'app1.example.com', roles: [:app, :web]
328
- host 'app2.example.com', roles: [:app, :web]
329
- host 'db.example.com', roles: [:db]
330
-
331
- # Set global variables
332
- set :app_name, 'my_app'
333
- set :deploy_to, '/var/www/${app_name}'
334
- set :keep_releases, 5
335
-
336
- # Define tasks
337
- task :check_requirements do
338
- run 'ruby -v'
339
- run 'node -v'
340
- run 'git --version'
341
- end
342
-
343
- task :setup_directories do
344
- run "mkdir -p ${deploy_to}"
345
- run "mkdir -p ${deploy_to}/releases"
346
- run "mkdir -p ${deploy_to}/shared"
347
- end
348
-
349
- task :deploy do
350
- depends_on :check_requirements, :setup_directories
351
-
352
- run 'git clone https://github.com/user/repo.git ${deploy_to}/releases/$(date +%Y%m%d%H%M%S)'
353
- run 'ln -sfn ${deploy_to}/releases/$(ls -t ${deploy_to}/releases | head -n1) ${deploy_to}/current'
354
- end
355
-
356
- task :restart_services do
357
- run 'sudo systemctl restart nginx'
358
- run 'sudo systemctl restart app'
359
- end
360
-
361
- task :cleanup do
362
- run "cd ${deploy_to}/releases && ls -t | tail -n +${keep_releases + 1} | xargs rm -rf"
363
- end
364
- end
365
- RUBY
366
-
367
- write_file("#{project_name}/deploy.rb", content)
368
- info "Created deployment script: #{project_name}/deploy.rb"
369
- end
370
-
371
- def create_inventory_file(project_name)
372
- content = <<~YAML
373
- # Server inventory configuration
374
- groups:
375
- production:
376
- hosts:
377
- - hostname: app1.example.com
378
- roles: [app, web]
379
- user: deploy
380
- port: 22
381
- - hostname: app2.example.com
382
- roles: [app, web]
383
- user: deploy
384
- port: 22
385
- - hostname: db.example.com
386
- roles: [db]
387
- user: deploy
388
- port: 22
389
- vars:
390
- db_name: production_db
391
- db_user: app_user
392
-
393
- staging:
394
- hosts:
395
- - hostname: staging.example.com
396
- roles: [app, web, db]
397
- user: deploy
398
- port: 22
399
- vars:
400
- rails_env: staging
401
- node_env: staging
402
- YAML
403
-
404
- write_file("#{project_name}/inventory.yml", content)
405
- info "Created inventory file: #{project_name}/inventory.yml"
406
- end
407
-
408
- def create_config_file(project_name)
409
- content = <<~YAML
410
- # Deployment configuration
411
- max_concurrent_tasks: 5
412
- retry_count: 3
413
- retry_delay: 5
414
- ssh_timeout: 30
415
- command_timeout: 300
416
-
417
- default_user: deploy
418
- default_port: 22
419
-
420
- template_dir: templates
421
- inventory_file: inventory.yml
422
-
423
- ssh_options:
424
- forward_agent: true
425
- verify_host_key: true
426
- keepalive: true
427
- keepalive_interval: 30
428
-
429
- logging:
430
- level: info
431
- file: kdeploy.log
432
- YAML
433
-
434
- write_file("#{project_name}/config/kdeploy.yml", content)
435
- info "Created configuration file: #{project_name}/config/kdeploy.yml"
436
- end
437
-
438
- def create_sample_scripts(project_name)
439
- create_setup_script(project_name)
440
- create_database_script(project_name)
441
- create_backup_script(project_name)
442
- create_monitoring_script(project_name)
443
- create_rollback_script(project_name)
444
- create_cleanup_script(project_name)
445
- end
446
-
447
- def create_setup_script(project_name)
448
- content = <<~RUBY
449
- # frozen_string_literal: true
450
-
451
- # Server setup script
452
- pipeline 'setup' do
453
- # Target all hosts
454
- host 'app1.example.com', roles: [:app, :web]
455
- host 'app2.example.com', roles: [:app, :web]
456
- host 'db.example.com', roles: [:db]
457
-
458
- # Set global variables
459
- set :ruby_version, '3.2.0'
460
- set :node_version, '18.x'
461
-
462
- task :install_dependencies do
463
- run <<~BASH
464
- # Update package lists
465
- sudo apt-get update
466
-
467
- # Install essential packages
468
- sudo apt-get install -y build-essential git curl
469
- sudo apt-get install -y nginx redis-server
470
- BASH
471
- end
472
-
473
- task :setup_ruby do
474
- run <<~BASH
475
- # Install rbenv
476
- git clone https://github.com/rbenv/rbenv.git ~/.rbenv
477
- echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
478
- echo 'eval "$(rbenv init -)"' >> ~/.bashrc
479
- source ~/.bashrc
480
-
481
- # Install ruby-build
482
- git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
483
-
484
- # Install Ruby
485
- rbenv install ${ruby_version}
486
- rbenv global ${ruby_version}
487
- BASH
488
- end
489
-
490
- task :setup_node do
491
- run <<~BASH
492
- # Install Node.js
493
- curl -fsSL https://deb.nodesource.com/setup_${node_version} | sudo -E bash -
494
- sudo apt-get install -y nodejs
495
-
496
- # Install Yarn
497
- curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
498
- echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
499
- sudo apt-get update && sudo apt-get install -y yarn
500
- BASH
501
- end
502
-
503
- task :setup_nginx do
504
- # Upload nginx configuration
505
- upload_template 'nginx.conf', '/etc/nginx/sites-available/app'
506
- run 'sudo ln -sf /etc/nginx/sites-available/app /etc/nginx/sites-enabled/app'
507
- run 'sudo nginx -t && sudo systemctl restart nginx'
508
- end
509
-
510
- task :setup_app_service do
511
- # Upload systemd service configuration
512
- upload_template 'app.service', '/etc/systemd/system/app.service'
513
- run 'sudo systemctl daemon-reload'
514
- run 'sudo systemctl enable app'
515
- end
516
-
517
- task :setup do
518
- depends_on :install_dependencies,
519
- :setup_ruby,
520
- :setup_node,
521
- :setup_nginx,
522
- :setup_app_service
523
- end
524
- end
525
- RUBY
526
-
527
- write_file("#{project_name}/scripts/setup.rb", content)
528
- info "Created setup script: #{project_name}/scripts/setup.rb"
529
- end
530
-
531
- def create_database_script(project_name)
532
- content = <<~RUBY
533
- # frozen_string_literal: true
534
-
535
- # Database management script
536
- pipeline 'database' do
537
- # Target database hosts
538
- host 'db.example.com', roles: [:db]
539
-
540
- # Set database configuration
541
- set :db_name, 'app_production'
542
- set :db_user, 'app_user'
543
- set :db_password, ENV['DB_PASSWORD']
544
-
545
- task :create_database do
546
- run <<~SQL
547
- psql -U postgres -c "CREATE USER ${db_user} WITH PASSWORD '${db_password}';"
548
- psql -U postgres -c "CREATE DATABASE ${db_name} OWNER ${db_user};"
549
- SQL
550
- end
551
-
552
- task :migrate do
553
- run 'cd /var/www/app/current && RAILS_ENV=production bundle exec rake db:migrate'
554
- end
555
-
556
- task :seed do
557
- run 'cd /var/www/app/current && RAILS_ENV=production bundle exec rake db:seed'
558
- end
559
-
560
- task :backup do
561
- run <<~BASH
562
- timestamp=$(date +%Y%m%d_%H%M%S)
563
- pg_dump -U ${db_user} ${db_name} > /var/backups/${db_name}_${timestamp}.sql
564
- gzip /var/backups/${db_name}_${timestamp}.sql
565
- BASH
566
- end
567
-
568
- task :restore do
569
- run <<~BASH
570
- latest_backup=$(ls -t /var/backups/${db_name}_*.sql.gz | head -n1)
571
- gunzip -c $latest_backup | psql -U ${db_user} ${db_name}
572
- BASH
573
- end
574
- end
575
- RUBY
576
-
577
- write_file("#{project_name}/scripts/database.rb", content)
578
- info "Created database script: #{project_name}/scripts/database.rb"
579
- end
580
-
581
- def create_backup_script(project_name)
582
- content = <<~RUBY
583
- # frozen_string_literal: true
584
-
585
- # Backup operations script
586
- pipeline 'backup' do
587
- # Target all hosts
588
- host 'app1.example.com', roles: [:app, :web]
589
- host 'app2.example.com', roles: [:app, :web]
590
- host 'db.example.com', roles: [:db]
591
-
592
- # Set backup configuration
593
- set :backup_dir, '/var/backups'
594
- set :keep_backups, 10
595
-
596
- task :backup_database do
597
- only :db
598
- run <<~BASH
599
- timestamp=$(date +%Y%m%d_%H%M%S)
600
- pg_dump -U ${db_user} ${db_name} > ${backup_dir}/${db_name}_${timestamp}.sql
601
- gzip ${backup_dir}/${db_name}_${timestamp}.sql
602
- BASH
603
- end
604
-
605
- task :backup_uploads do
606
- only [:app, :web]
607
- run <<~BASH
608
- timestamp=$(date +%Y%m%d_%H%M%S)
609
- tar -czf ${backup_dir}/uploads_${timestamp}.tar.gz /var/www/app/shared/public/uploads
610
- BASH
611
- end
612
-
613
- task :backup_logs do
614
- run <<~BASH
615
- timestamp=$(date +%Y%m%d_%H%M%S)
616
- tar -czf ${backup_dir}/logs_${timestamp}.tar.gz /var/log/app
617
- BASH
618
- end
619
-
620
- task :cleanup_old_backups do
621
- run <<~BASH
622
- cd ${backup_dir}
623
- ls -t *.sql.gz | tail -n +${keep_backups + 1} | xargs rm -f
624
- ls -t *.tar.gz | tail -n +${keep_backups + 1} | xargs rm -f
625
- BASH
626
- end
627
-
628
- task :backup do
629
- depends_on :backup_database,
630
- :backup_uploads,
631
- :backup_logs,
632
- :cleanup_old_backups
633
- end
634
- end
635
- RUBY
636
-
637
- write_file("#{project_name}/scripts/backup.rb", content)
638
- info "Created backup script: #{project_name}/scripts/backup.rb"
639
- end
640
-
641
- def create_monitoring_script(project_name)
642
- content = <<~RUBY
643
- # frozen_string_literal: true
644
-
645
- # Health checks and monitoring script
646
- pipeline 'monitoring' do
647
- # Target all hosts
648
- host 'app1.example.com', roles: [:app, :web]
649
- host 'app2.example.com', roles: [:app, :web]
650
- host 'db.example.com', roles: [:db]
651
-
652
- task :check_system_resources do
653
- run <<~BASH
654
- echo "Memory Usage:"
655
- free -h
656
- echo "\\nDisk Usage:"
657
- df -h
658
- echo "\\nCPU Load:"
659
- uptime
660
- BASH
661
- end
662
-
663
- task :check_services do
664
- run <<~BASH
665
- echo "Nginx Status:"
666
- sudo systemctl status nginx
667
- echo "\\nApp Status:"
668
- sudo systemctl status app
669
- echo "\\nRedis Status:"
670
- sudo systemctl status redis-server
671
- BASH
672
- end
673
-
674
- task :check_logs do
675
- run <<~BASH
676
- echo "Last 50 lines of application log:"
677
- tail -n 50 /var/www/app/current/log/production.log
678
- echo "\\nLast 50 lines of nginx error log:"
679
- sudo tail -n 50 /var/log/nginx/error.log
680
- BASH
681
- end
682
-
683
- task :check_database do
684
- only :db
685
- run <<~BASH
686
- echo "PostgreSQL Status:"
687
- sudo systemctl status postgresql
688
- echo "\\nDatabase Size:"
689
- psql -U ${db_user} -d ${db_name} -c "\\l+"
690
- BASH
691
- end
692
-
693
- task :monitor do
694
- depends_on :check_system_resources,
695
- :check_services,
696
- :check_logs,
697
- :check_database
698
- end
699
- end
700
- RUBY
701
-
702
- write_file("#{project_name}/scripts/monitoring.rb", content)
703
- info "Created monitoring script: #{project_name}/scripts/monitoring.rb"
68
+ desc 'init [DIR]', 'Initialize a new deployment project'
69
+ def init(dir = '.')
70
+ initializer = Initializer.new(dir)
71
+ initializer.run
72
+ rescue StandardError => e
73
+ puts Kdeploy::Banner.show_error(e.message)
74
+ exit 1
704
75
  end
705
76
 
706
- def create_rollback_script(project_name)
707
- content = <<~RUBY
708
- # frozen_string_literal: true
709
-
710
- # Rollback operations script
711
- pipeline 'rollback' do
712
- # Target application hosts
713
- host 'app1.example.com', roles: [:app, :web]
714
- host 'app2.example.com', roles: [:app, :web]
715
-
716
- # Set deployment configuration
717
- set :app_name, 'my_app'
718
- set :deploy_to, '/var/www/${app_name}'
719
-
720
- task :list_releases do
721
- run "ls -lt ${deploy_to}/releases"
722
- end
723
-
724
- task :rollback_code do
725
- run <<~BASH
726
- current_release=$(readlink ${deploy_to}/current)
727
- previous_release=$(ls -t ${deploy_to}/releases | head -n 2 | tail -n 1)
728
- ln -sfn ${deploy_to}/releases/$previous_release ${deploy_to}/current
729
- BASH
730
- end
77
+ desc 'execute TASK_FILE [TASK]', 'Execute deployment tasks from file'
78
+ method_option :limit, type: :string, desc: 'Limit to specific hosts (comma-separated)'
79
+ method_option :parallel, type: :numeric, default: 5, desc: 'Number of parallel executions'
80
+ method_option :dry_run, type: :boolean, desc: 'Show what would be done'
81
+ def execute(task_file, task_name = nil)
82
+ load_task_file(task_file)
731
83
 
732
- task :rollback_database do
733
- run 'cd ${deploy_to}/current && RAILS_ENV=production bundle exec rake db:rollback STEP=1'
734
- end
84
+ tasks_to_run = if task_name
85
+ [task_name.to_sym]
86
+ else
87
+ self.class.kdeploy_tasks.keys
88
+ end
735
89
 
736
- task :restart_services do
737
- run 'sudo systemctl restart app'
738
- run 'sudo systemctl restart nginx'
739
- end
90
+ tasks_to_run.each do |task|
91
+ task_hosts = self.class.get_task_hosts(task)
92
+ hosts = filter_hosts(options[:limit], task_hosts)
740
93
 
741
- task :rollback do
742
- depends_on :list_releases,
743
- :rollback_code,
744
- :rollback_database,
745
- :restart_services
746
- end
94
+ if hosts.empty?
95
+ puts Kdeploy::Banner.show_error("No hosts found for task: #{task}")
96
+ next
747
97
  end
748
- RUBY
749
-
750
- write_file("#{project_name}/scripts/rollback.rb", content)
751
- info "Created rollback script: #{project_name}/scripts/rollback.rb"
752
- end
753
98
 
754
- def create_cleanup_script(project_name)
755
- content = <<~RUBY
756
- # frozen_string_literal: true
757
-
758
- # Cleanup operations script
759
- pipeline 'cleanup' do
760
- # Target all hosts
761
- host 'app1.example.com', roles: [:app, :web]
762
- host 'app2.example.com', roles: [:app, :web]
763
- host 'db.example.com', roles: [:db]
764
-
765
- # Set cleanup configuration
766
- set :app_name, 'my_app'
767
- set :deploy_to, '/var/www/${app_name}'
768
- set :keep_releases, 5
769
- set :keep_logs, 7
770
-
771
- task :cleanup_releases do
772
- run <<~BASH
773
- cd ${deploy_to}/releases
774
- ls -t | tail -n +${keep_releases + 1} | xargs rm -rf
775
- BASH
776
- end
777
-
778
- task :cleanup_logs do
779
- run <<~BASH
780
- find /var/www/app/current/log -name "*.log.*" -mtime +${keep_logs} -exec rm {} \\;
781
- find /var/log/nginx -name "*.log.*" -mtime +${keep_logs} -exec sudo rm {} \\;
782
- BASH
783
- end
784
-
785
- task :cleanup_temp do
786
- run <<~BASH
787
- find /tmp -name "${app_name}-*" -mtime +1 -exec rm -rf {} \\;
788
- find /var/tmp -name "${app_name}-*" -mtime +1 -exec rm -rf {} \\;
789
- BASH
790
- end
791
-
792
- task :cleanup do
793
- depends_on :cleanup_releases,
794
- :cleanup_logs,
795
- :cleanup_temp
796
- end
99
+ if options[:dry_run]
100
+ print_dry_run(hosts, task)
101
+ next
797
102
  end
798
- RUBY
799
-
800
- write_file("#{project_name}/scripts/cleanup.rb", content)
801
- info "Created cleanup script: #{project_name}/scripts/cleanup.rb"
802
- end
803
103
 
804
- def create_sample_templates(project_name)
805
- create_nginx_template(project_name)
806
- create_app_service_template(project_name)
807
- create_deploy_script_template(project_name)
808
- create_backup_script_template(project_name)
809
- end
810
-
811
- def create_nginx_template(project_name)
812
- content = <<~ERB
813
- # Nginx configuration for <%= app_name %>
814
- upstream app_server {
815
- server unix:/var/www/<%= app_name %>/shared/tmp/sockets/puma.sock fail_timeout=0;
816
- }
817
-
818
- server {
819
- listen 80;
820
- server_name <%= server_name %>;
821
- root /var/www/<%= app_name %>/current/public;
822
-
823
- location ^~ /assets/ {
824
- gzip_static on;
825
- expires max;
826
- add_header Cache-Control public;
827
- }
828
-
829
- try_files $uri/index.html $uri @app;
830
-
831
- location @app {
832
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
833
- proxy_set_header Host $http_host;
834
- proxy_redirect off;
835
- proxy_pass http://app_server;
836
- }
837
-
838
- error_page 500 502 503 504 /500.html;
839
- client_max_body_size 4G;
840
- keepalive_timeout 10;
841
- }
842
- ERB
843
-
844
- write_file("#{project_name}/templates/nginx.conf.erb", content)
845
- info "Created nginx template: #{project_name}/templates/nginx.conf.erb"
846
- end
847
-
848
- def create_app_service_template(project_name)
849
- content = <<~ERB
850
- [Unit]
851
- Description=<%= app_name %> application server
852
- After=network.target
853
-
854
- [Service]
855
- Type=simple
856
- User=<%= app_user %>
857
- WorkingDirectory=/var/www/<%= app_name %>/current
858
- Environment=RAILS_ENV=production
859
- Environment=PATH=/home/<%= app_user %>/.rbenv/shims:/usr/local/bin:/usr/bin:/bin
860
- ExecStart=/home/<%= app_user %>/.rbenv/shims/bundle exec puma -C config/puma.rb
861
- Restart=always
862
- RestartSec=1
863
-
864
- [Install]
865
- WantedBy=multi-user.target
866
- ERB
867
-
868
- write_file("#{project_name}/templates/app.service.erb", content)
869
- info "Created app service template: #{project_name}/templates/app.service.erb"
870
- end
871
-
872
- def create_deploy_script_template(project_name)
873
- content = <<~ERB
874
- #!/bin/bash
875
- # Deployment script for <%= app_name %>
876
-
877
- set -e
878
-
879
- APP_ROOT="/var/www/<%= app_name %>"
880
- CURRENT="$APP_ROOT/current"
881
- SHARED="$APP_ROOT/shared"
882
- RELEASE="$APP_ROOT/releases/$(date +%Y%m%d%H%M%S)"
883
-
884
- echo "Deploying <%= app_name %> to $RELEASE"
885
-
886
- # Create release directory
887
- mkdir -p $RELEASE
888
-
889
- # Clone repository
890
- git clone <%= repository_url %> $RELEASE
891
-
892
- # Install dependencies
893
- cd $RELEASE
894
- bundle install --deployment --without development test
895
- yarn install --production
896
-
897
- # Compile assets
898
- bundle exec rake assets:precompile RAILS_ENV=production
899
-
900
- # Create symlinks
901
- ln -s $SHARED/config/database.yml $RELEASE/config/database.yml
902
- ln -s $SHARED/config/master.key $RELEASE/config/master.key
903
- ln -s $SHARED/public/uploads $RELEASE/public/uploads
904
-
905
- # Update current symlink
906
- ln -sfn $RELEASE $CURRENT
907
-
908
- # Restart application
909
- sudo systemctl restart <%= app_name %>
910
-
911
- echo "Deployment completed successfully!"
912
- ERB
913
-
914
- write_file("#{project_name}/templates/deploy.sh.erb", content)
915
- info "Created deploy script template: #{project_name}/templates/deploy.sh.erb"
916
- end
917
-
918
- def create_backup_script_template(project_name)
919
- content = <<~ERB
920
- #!/bin/bash
921
- # Backup script for <%= app_name %>
922
-
923
- set -e
924
-
925
- TIMESTAMP=$(date +%Y%m%d_%H%M%S)
926
- BACKUP_DIR="<%= backup_dir %>"
927
- DB_NAME="<%= db_name %>"
928
- APP_ROOT="/var/www/<%= app_name %>"
929
-
930
- echo "Starting backup for <%= app_name %>"
931
-
932
- # Create backup directory
933
- mkdir -p $BACKUP_DIR
934
-
935
- # Backup database
936
- echo "Backing up database..."
937
- pg_dump -U <%= db_user %> $DB_NAME > $BACKUP_DIR/${DB_NAME}_${TIMESTAMP}.sql
938
- gzip $BACKUP_DIR/${DB_NAME}_${TIMESTAMP}.sql
939
-
940
- # Backup uploads
941
- echo "Backing up uploads..."
942
- tar -czf $BACKUP_DIR/uploads_${TIMESTAMP}.tar.gz $APP_ROOT/shared/public/uploads
943
-
944
- # Backup configuration
945
- echo "Backing up configuration..."
946
- tar -czf $BACKUP_DIR/config_${TIMESTAMP}.tar.gz $APP_ROOT/shared/config
947
-
948
- # Cleanup old backups
949
- echo "Cleaning up old backups..."
950
- find $BACKUP_DIR -name "*.sql.gz" -mtime +<%= keep_days %> -delete
951
- find $BACKUP_DIR -name "*.tar.gz" -mtime +<%= keep_days %> -delete
952
-
953
- echo "Backup completed successfully!"
954
- ERB
955
-
956
- write_file("#{project_name}/templates/backup.sh.erb", content)
957
- info "Created backup script template: #{project_name}/templates/backup.sh.erb"
958
- end
959
-
960
- def write_file(path, content)
961
- File.write(path, content)
962
- end
963
-
964
- def determine_project_name(project_name)
965
- project_name || options[:name] || File.basename(Dir.pwd)
966
- end
967
-
968
- def display_init_header(project_name)
969
- puts ''
970
- puts "🆕 Initializing kdeploy project: #{project_name}".colorize(:yellow)
971
- puts ''
972
- end
973
-
974
- def display_init_success(project_name)
975
- puts ''
976
- puts '✅ Project initialized successfully!'.colorize(:green)
977
- puts ''
978
- display_project_structure(project_name)
979
- display_next_steps(project_name)
980
- display_available_scripts
981
- end
982
-
983
- def display_project_structure(project_name)
984
- puts '📁 Project Structure Created:'.colorize(:cyan)
985
- puts " #{project_name}/".colorize(:light_blue)
986
- puts ' ├── deploy.rb # Main deployment script'.colorize(:light_blue)
987
- puts ' ├── inventory.yml # Server inventory'.colorize(:light_blue)
988
- puts ' ├── config/ # Configuration files'.colorize(:light_blue)
989
- puts ' │ └── kdeploy.yml # Deployment configuration'.colorize(:light_blue)
990
- puts ' ├── scripts/ # Additional scripts'.colorize(:light_blue)
991
- puts ' │ ├── setup.rb # Server setup script'.colorize(:light_blue)
992
- puts ' │ ├── database.rb # Database management'.colorize(:light_blue)
993
- puts ' │ ├── backup.rb # Backup operations'.colorize(:light_blue)
994
- puts ' │ ├── monitoring.rb # Health checks & monitoring'.colorize(:light_blue)
995
- puts ' │ ├── rollback.rb # Rollback operations'.colorize(:light_blue)
996
- puts ' │ └── cleanup.rb # Cleanup operations'.colorize(:light_blue)
997
- puts ' └── templates/ # Configuration templates'.colorize(:light_blue)
998
- puts ' ├── nginx.conf.erb # Nginx configuration'.colorize(:light_blue)
999
- puts ' ├── app.service.erb # Systemd service'.colorize(:light_blue)
1000
- puts ' ├── deploy.sh.erb # Deployment script'.colorize(:light_blue)
1001
- puts ' └── backup.sh.erb # Backup script'.colorize(:light_blue)
1002
- puts ''
1003
- end
1004
-
1005
- def display_next_steps(project_name)
1006
- puts '🚀 Next Steps:'.colorize(:cyan)
1007
- puts " 1. cd #{project_name}".colorize(:light_blue)
1008
- puts ' 2. Edit deploy.rb to configure your deployment'.colorize(:light_blue)
1009
- puts ' 3. Update inventory.yml with your servers'.colorize(:light_blue)
1010
- puts ' 4. Run: kdeploy deploy scripts/setup.rb # Setup servers'.colorize(:light_blue)
1011
- puts ' 5. Run: kdeploy deploy deploy.rb # Deploy application'.colorize(:light_blue)
1012
- puts ''
1013
- end
1014
-
1015
- def display_available_scripts
1016
- puts '💡 Available Scripts:'.colorize(:cyan)
1017
- puts ' kdeploy deploy scripts/setup.rb # Initial server setup'.colorize(:light_blue)
1018
- puts ' kdeploy deploy scripts/database.rb # Database operations'.colorize(:light_blue)
1019
- puts ' kdeploy deploy scripts/backup.rb # Backup operations'.colorize(:light_blue)
1020
- puts ' kdeploy deploy scripts/monitoring.rb # Health checks'.colorize(:light_blue)
1021
- puts ' kdeploy deploy scripts/rollback.rb # Rollback operations'.colorize(:light_blue)
1022
- puts ' kdeploy deploy scripts/cleanup.rb # Cleanup operations'.colorize(:light_blue)
1023
- puts ''
1024
- puts '💡 Need help? Run: kdeploy help deploy'.colorize(:yellow)
1025
- puts ''
1026
- end
1027
-
1028
- def validate_script_file(script_file)
1029
- return if File.exist?(script_file)
1030
-
1031
- error "Script file not found: #{script_file}"
1032
- exit 1
1033
- end
1034
-
1035
- def display_validation_header(script_file)
1036
- puts ''
1037
- puts "🔍 Validating deployment script: #{script_file}".colorize(:yellow)
1038
- puts ''
1039
- end
1040
-
1041
- def display_validation_success(pipeline)
1042
- puts '✅ Script validation passed'.colorize(:green)
1043
- puts ''
1044
- display_pipeline_summary(pipeline)
1045
- display_target_hosts(pipeline.hosts) if pipeline.hosts.any?
1046
- display_tasks(pipeline.tasks) if pipeline.tasks.any?
1047
- puts ''
1048
- end
1049
-
1050
- def display_pipeline_summary(pipeline)
1051
- puts '📋 Pipeline Summary:'.colorize(:cyan)
1052
- puts " Name: #{pipeline.name}".colorize(:light_blue)
1053
- puts " Hosts: #{pipeline.hosts.size}".colorize(:light_blue)
1054
- puts " Tasks: #{pipeline.tasks.size}".colorize(:light_blue)
1055
- end
1056
-
1057
- def display_target_hosts(hosts)
1058
- puts ''
1059
- puts '🖥️ Target Hosts:'.colorize(:cyan)
1060
- hosts.each do |host|
1061
- puts " • #{host.hostname}:#{host.port} (#{host.user})".colorize(:light_blue)
1062
- end
1063
- end
1064
-
1065
- def display_tasks(tasks)
1066
- puts ''
1067
- puts '🔧 Tasks to Execute:'.colorize(:cyan)
1068
- tasks.each_with_index do |task, index|
1069
- puts " #{index + 1}. #{task.name}".colorize(:light_blue)
104
+ runner = Runner.new(hosts, self.class.kdeploy_tasks, parallel: options[:parallel])
105
+ results = runner.run(task)
106
+ print_results(results, task)
1070
107
  end
1071
- end
1072
-
1073
- def display_validation_errors(errors)
1074
- puts '❌ Script validation failed:'.colorize(:red)
1075
- puts ''
1076
- errors.each { |err| puts " • #{err}".colorize(:red) }
1077
- puts ''
108
+ rescue StandardError => e
109
+ puts Kdeploy::Banner.show_error(e.message)
1078
110
  exit 1
1079
111
  end
1080
112
 
1081
- def display_validation_failure(error)
1082
- puts "❌ Validation failed: #{error.message}".colorize(:red)
1083
- puts ''
1084
- exit 1
1085
- end
1086
-
1087
- def handle_deployment_error(error)
1088
- error "Deployment failed: #{error.message}"
1089
- exit 1
1090
- end
1091
-
1092
- def handle_unexpected_error(error)
1093
- error "Unexpected error: #{error.message}"
1094
- KdeployLogger.debug("Backtrace: #{error.backtrace.join("\n")}")
1095
- exit 1
1096
- end
1097
-
1098
- def show_summary_stats
1099
- show_kdeploy_banner
1100
- stats = Kdeploy.statistics
1101
- deployment_summary = stats.deployment_summary(days: options[:days])
1102
- task_summary = stats.task_summary(days: options[:days])
1103
- global_summary = stats.global_summary
1104
-
1105
- case options[:format].downcase
1106
- when 'json'
1107
- display_json_summary(deployment_summary, task_summary, global_summary)
1108
- else
1109
- print_summary_table(deployment_summary, task_summary, global_summary)
1110
- end
1111
- end
1112
-
1113
- def show_deployment_stats
1114
- show_kdeploy_banner
1115
- stats = Kdeploy.statistics
1116
- summary = stats.deployment_summary(days: options[:days])
1117
-
1118
- case options[:format].downcase
1119
- when 'json'
1120
- puts JSON.pretty_generate(summary)
1121
- else
1122
- print_deployment_table(summary)
1123
- end
1124
- end
1125
-
1126
- def show_task_stats
1127
- show_kdeploy_banner
1128
- stats = Kdeploy.statistics
1129
- summary = stats.task_summary(days: options[:days])
1130
-
1131
- case options[:format].downcase
1132
- when 'json'
1133
- puts JSON.pretty_generate(summary)
1134
- else
1135
- print_task_table(summary)
1136
- end
1137
- end
1138
-
1139
- def show_failure_stats
1140
- show_kdeploy_banner
1141
- stats = Kdeploy.statistics
1142
- failed_tasks = stats.top_failed_tasks(limit: 10, days: options[:days])
113
+ private
1143
114
 
1144
- case options[:format].downcase
1145
- when 'json'
1146
- puts JSON.pretty_generate(failed_tasks)
1147
- else
1148
- print_failure_table(failed_tasks)
115
+ def load_task_file(file)
116
+ unless File.exist?(file)
117
+ puts Kdeploy::Banner.show_error("Task file not found: #{file}")
118
+ exit 1
1149
119
  end
1150
- end
1151
120
 
1152
- def show_trend_stats
1153
- show_kdeploy_banner
1154
- stats = Kdeploy.statistics
1155
- trends = stats.performance_trends(days: options[:days])
1156
-
1157
- case options[:format].downcase
1158
- when 'json'
1159
- puts JSON.pretty_generate(trends)
1160
- else
1161
- print_trend_table(trends)
1162
- end
121
+ self.class.class_eval(File.read(file), file)
1163
122
  end
1164
123
 
1165
- def show_global_stats
1166
- show_kdeploy_banner
1167
- stats = Kdeploy.statistics
1168
- global_summary = stats.global_summary
124
+ def filter_hosts(limit, task_hosts)
125
+ hosts = self.class.kdeploy_hosts.slice(*task_hosts)
126
+ return hosts unless limit
1169
127
 
1170
- case options[:format].downcase
1171
- when 'json'
1172
- puts JSON.pretty_generate(global_summary)
1173
- else
1174
- print_global_table(global_summary)
1175
- end
1176
- end
1177
-
1178
- def clear_statistics
1179
- show_kdeploy_banner
1180
- prompt = TTY::Prompt.new
1181
- if prompt.yes?('Are you sure you want to clear all statistics? This cannot be undone.')
1182
- Kdeploy.statistics.clear_statistics!
1183
- success 'Statistics cleared successfully'
1184
- else
1185
- info 'Statistics clearing cancelled'
1186
- end
128
+ host_names = limit.split(',').map(&:strip)
129
+ hosts.slice(*host_names)
1187
130
  end
1188
131
 
1189
- def export_statistics
1190
- show_kdeploy_banner
1191
- export_file = options[:output] || generate_export_filename
1192
- format = determine_export_format(export_file)
1193
-
1194
- Kdeploy.statistics.export_statistics(export_file, format: format)
1195
- success "Statistics exported to #{export_file}"
1196
- end
1197
-
1198
- def display_json_summary(deployment_summary, task_summary, global_summary)
1199
- puts JSON.pretty_generate(
1200
- {
1201
- deployment_summary: deployment_summary,
1202
- task_summary: task_summary,
1203
- global_summary: global_summary
132
+ def print_dry_run(hosts, task_name)
133
+ puts Kdeploy::Banner.show
134
+ pastel = Pastel.new
135
+ puts TTY::Box.frame(
136
+ 'Showing what would be done without executing any commands',
137
+ title: { top_left: ' Dry Run Mode ', bottom_right: ' Kdeploy ' },
138
+ style: {
139
+ border: {
140
+ fg: :blue
141
+ }
1204
142
  }
1205
143
  )
1206
- end
1207
-
1208
- def print_summary_table(deployment_summary, task_summary, global_summary)
1209
- puts "\n📊 Kdeploy Statistics Summary (Last #{options[:days]} days)".colorize(:cyan)
1210
- puts '=' * 60
1211
-
1212
- print_deployment_section(deployment_summary)
1213
- print_task_section(task_summary)
1214
- print_global_section(global_summary)
1215
- end
1216
-
1217
- def print_deployment_section(summary)
1218
- puts "\n📦 Deployment Statistics".colorize(:yellow)
1219
- if summary[:total_deployments].positive?
1220
- puts " Total Deployments: #{summary[:total_deployments]}"
1221
- puts " Successful: #{summary[:successful_deployments]} (#{summary[:success_rate]}%)"
1222
- puts " Failed: #{summary[:failed_deployments]}"
1223
- puts " Average Duration: #{summary[:avg_duration]}s"
1224
- puts " Total Duration: #{format_duration(summary[:total_duration])}"
1225
- else
1226
- puts " No deployments in the last #{options[:days]} days"
1227
- end
1228
- end
1229
-
1230
- def print_task_section(summary)
1231
- puts "\n🔧 Task Statistics".colorize(:yellow)
1232
- if summary[:total_task_executions].positive?
1233
- puts " Total Task Executions: #{summary[:total_task_executions]}"
1234
- puts " Unique Tasks: #{summary[:unique_tasks]}"
1235
- print_top_tasks(summary[:tasks]) if summary[:tasks].any?
1236
- else
1237
- puts " No task executions in the last #{options[:days]} days"
1238
- end
1239
- end
1240
-
1241
- def print_top_tasks(tasks)
1242
- puts ' Top Tasks:'
1243
- sorted_tasks = tasks.sort_by { |_, stats| -stats[:total_executions] }.first(5)
1244
- sorted_tasks.each do |name, stats|
1245
- puts " #{name}: #{stats[:total_executions]} executions (#{stats[:success_rate]}% success)"
1246
- end
1247
- end
1248
-
1249
- def print_global_section(summary)
1250
- puts "\n🌍 Global Statistics".colorize(:yellow)
1251
- puts " Total Deployments: #{summary[:total_deployments]}"
1252
- puts " Total Tasks: #{summary[:total_tasks]}"
1253
- puts " Total Commands: #{summary[:total_commands]}"
1254
- puts " Total Execution Time: #{format_duration(summary[:total_execution_time])}"
1255
- puts " Session Duration: #{format_duration(summary[:session_duration])}"
1256
- puts ''
1257
- end
1258
-
1259
- def print_deployment_table(summary)
1260
- puts "\n📦 Deployment Statistics (Last #{options[:days]} days)".colorize(:cyan)
1261
- puts '=' * 60
1262
-
1263
- if summary[:total_deployments].zero?
1264
- puts 'No deployments found'
1265
- return
1266
- end
1267
-
1268
- display_deployment_stats(summary)
1269
- end
1270
-
1271
- def display_deployment_stats(summary)
1272
- puts "Total Deployments: #{summary[:total_deployments]}"
1273
- puts "Successful: #{summary[:successful_deployments]} (#{summary[:success_rate]}%)"
1274
- puts "Failed: #{summary[:failed_deployments]}"
1275
- puts "Average Duration: #{summary[:avg_duration]}s"
1276
- puts "Min Duration: #{summary[:min_duration]}s"
1277
- puts "Max Duration: #{summary[:max_duration]}s"
1278
- puts "Total Duration: #{format_duration(summary[:total_duration])}"
1279
- puts ''
1280
- end
1281
-
1282
- def print_task_table(summary)
1283
- puts "\n🔧 Task Statistics (Last #{options[:days]} days)".colorize(:cyan)
1284
- puts '=' * 60
1285
-
1286
- if summary[:total_task_executions].zero?
1287
- puts 'No task executions found'
1288
- return
1289
- end
1290
-
1291
- display_task_summary(summary)
1292
- display_task_details(summary[:tasks]) if summary[:tasks].any?
1293
- end
1294
-
1295
- def display_task_summary(summary)
1296
- puts "Total Executions: #{summary[:total_task_executions]}"
1297
- puts "Unique Tasks: #{summary[:unique_tasks]}"
1298
- puts ''
1299
- end
1300
-
1301
- def display_task_details(tasks)
1302
- puts 'Task Details:'
1303
- print_task_header
1304
- tasks.each { |name, stats| print_task_row(name, stats) }
1305
- puts ''
1306
- end
1307
-
1308
- def print_task_header
1309
- printf "%-30s %10s %10s %10s %12s %12s\n",
1310
- 'Task Name', 'Executions', 'Success', 'Failed', 'Success Rate', 'Avg Duration'
1311
- puts '-' * 95
1312
- end
1313
-
1314
- def print_task_row(name, stats)
1315
- printf "%-30s %10d %10d %10d %11.1f%% %11.2fs\n",
1316
- name.truncate(28),
1317
- stats[:total_executions],
1318
- stats[:successful],
1319
- stats[:failed],
1320
- stats[:success_rate],
1321
- stats[:avg_duration]
1322
- end
1323
-
1324
- def print_failure_table(failed_tasks)
1325
- puts "\n❌ Top Failed Tasks (Last #{options[:days]} days)".colorize(:red)
1326
- puts '=' * 60
1327
-
1328
- if failed_tasks.empty?
1329
- puts 'No failed tasks found'
1330
- return
1331
- end
1332
-
1333
- print_failure_header
1334
- failed_tasks.each { |task| print_failure_row(task) }
1335
- puts ''
1336
- end
1337
-
1338
- def print_failure_header
1339
- printf "%-30s %10s %20s\n", 'Task Name', 'Failures', 'Last Failure'
1340
- puts '-' * 62
1341
- end
1342
-
1343
- def print_failure_row(task)
1344
- last_failure_time = Time.at(task[:last_failure][:timestamp]).strftime('%Y-%m-%d %H:%M:%S')
1345
- printf "%-30s %10d %20s\n",
1346
- task[:task_name].truncate(28),
1347
- task[:failure_count],
1348
- last_failure_time
1349
- end
1350
-
1351
- def print_trend_table(trends)
1352
- puts "\n📈 Performance Trends (Last #{options[:days]} days)".colorize(:cyan)
1353
- puts '=' * 80
1354
-
1355
- if trends[:trends].empty?
1356
- puts 'No trend data available'
1357
- return
1358
- end
1359
-
1360
- print_trend_header
1361
- trends[:trends].each { |date, stats| print_trend_row(date, stats) }
1362
- puts ''
1363
- end
1364
-
1365
- def print_trend_header
1366
- printf "%-12s %10s %10s %10s %12s %12s\n",
1367
- 'Date', 'Total', 'Success', 'Failed', 'Success Rate', 'Avg Duration'
1368
- puts '-' * 78
1369
- end
1370
-
1371
- def print_trend_row(date, stats)
1372
- printf "%-12s %10d %10d %10d %11.1f%% %11.2fs\n",
1373
- date,
1374
- stats[:total],
1375
- stats[:successful],
1376
- stats[:failed],
1377
- stats[:success_rate],
1378
- stats[:avg_duration]
1379
- end
1380
-
1381
- def print_global_table(global_summary)
1382
- puts "\n🌍 Global Statistics".colorize(:cyan)
1383
- puts '=' * 60
1384
-
1385
- print_global_deployment_stats(global_summary)
1386
- print_global_task_stats(global_summary)
1387
- print_global_command_stats(global_summary)
1388
- print_global_execution_stats(global_summary)
1389
- end
1390
-
1391
- def print_global_deployment_stats(summary)
1392
- puts 'Deployments:'
1393
- puts " Total: #{summary[:total_deployments]}"
1394
- puts " Successful: #{summary[:successful_deployments]}"
1395
- puts " Failed: #{summary[:failed_deployments]}"
1396
- end
1397
-
1398
- def print_global_task_stats(summary)
1399
- puts "\nTasks:"
1400
- puts " Total: #{summary[:total_tasks]}"
1401
- puts " Successful: #{summary[:successful_tasks]}"
1402
- puts " Failed: #{summary[:failed_tasks]}"
1403
- end
1404
-
1405
- def print_global_command_stats(summary)
1406
- puts "\nCommands:"
1407
- puts " Total: #{summary[:total_commands]}"
1408
- puts " Successful: #{summary[:successful_commands]}"
1409
- puts " Failed: #{summary[:failed_commands]}"
1410
- end
1411
-
1412
- def print_global_execution_stats(summary)
1413
- puts "\nExecution Time:"
1414
- puts " Total: #{format_duration(summary[:total_execution_time])}"
1415
- puts " Session: #{format_duration(summary[:session_duration])}"
1416
- puts " Session Started: #{summary[:session_start_time].strftime('%Y-%m-%d %H:%M:%S')}"
1417
- puts ''
1418
- end
1419
-
1420
- def format_duration(seconds)
1421
- return '0s' if seconds.nil? || seconds.zero?
1422
-
1423
- if seconds < 60
1424
- "#{seconds.round(1)}s"
1425
- elsif seconds < 3600
1426
- format_minutes(seconds)
1427
- else
1428
- format_hours(seconds)
144
+ puts
145
+
146
+ hosts.each do |name, config|
147
+ commands = self.class.kdeploy_tasks[task_name][:block].call
148
+ output = commands.map do |command|
149
+ case command[:type]
150
+ when :run
151
+ "#{pastel.green('>')} #{command[:command]}"
152
+ when :upload
153
+ "#{pastel.blue('>')} Upload: #{command[:source]} -> #{command[:destination]}"
154
+ end
155
+ end.join("\n")
156
+
157
+ puts TTY::Box.frame(
158
+ output,
159
+ title: { top_left: " #{name} (#{config[:ip]}) " },
160
+ style: {
161
+ border: {
162
+ fg: :yellow
163
+ }
164
+ }
165
+ )
166
+ puts
167
+ end
168
+ end
169
+
170
+ def print_results(results, task_name)
171
+ puts Kdeploy::Banner.show
172
+ pastel = Pastel.new
173
+
174
+ puts "#{pastel.bright_cyan('Task:')} #{pastel.bright_white(task_name)}"
175
+ puts
176
+
177
+ results.each do |host, result|
178
+ status = if result[:status] == :success
179
+ pastel.green('✓ Success')
180
+ else
181
+ pastel.red('✗ Failed')
182
+ end
183
+
184
+ puts "#{pastel.bright_white(host)} - #{status}"
185
+
186
+ if result[:status] == :success
187
+ result[:output].each do |cmd|
188
+ puts " #{pastel.bright_yellow('$')} #{cmd[:command]}"
189
+ if cmd[:output].is_a?(Hash)
190
+ unless cmd[:output][:stdout].empty?
191
+ cmd[:output][:stdout].each_line do |line|
192
+ line = line.strip
193
+ next if line.empty?
194
+
195
+ # 根据输出内容的特征来决定颜色
196
+ colored_line = if line.match?(/error|fail|fatal|critical/i)
197
+ pastel.red(line)
198
+ elsif line.match?(/warn|deprecat|notice/i)
199
+ pastel.yellow(line)
200
+ else
201
+ pastel.green(line)
202
+ end
203
+ puts " #{colored_line}"
204
+ end
205
+ end
206
+ unless cmd[:output][:stderr].empty?
207
+ cmd[:output][:stderr].each_line do |line|
208
+ puts " #{pastel.red(line)}" unless line.strip.empty?
209
+ end
210
+ end
211
+ elsif cmd[:output]
212
+ cmd[:output].each_line do |line|
213
+ line = line.strip
214
+ next if line.empty?
215
+
216
+ # 根据输出内容的特征来决定颜色
217
+ colored_line = if line.match?(/error|fail|fatal|critical/i)
218
+ pastel.red(line)
219
+ elsif line.match?(/warn|deprecat|notice/i)
220
+ pastel.yellow(line)
221
+ else
222
+ pastel.green(line)
223
+ end
224
+ puts " #{colored_line}"
225
+ end
226
+ end
227
+ puts
228
+ end
229
+ else
230
+ puts " #{pastel.red(result[:error])}"
231
+ end
232
+ puts
1429
233
  end
1430
234
  end
1431
-
1432
- def format_minutes(seconds)
1433
- minutes = (seconds / 60).to_i
1434
- remaining_seconds = (seconds % 60).to_i
1435
- "#{minutes}m #{remaining_seconds}s"
1436
- end
1437
-
1438
- def format_hours(seconds)
1439
- hours = (seconds / 3600).to_i
1440
- remaining_minutes = ((seconds % 3600) / 60).to_i
1441
- "#{hours}h #{remaining_minutes}m"
1442
- end
1443
-
1444
- def generate_export_filename
1445
- "kdeploy_stats_#{Time.now.strftime('%Y%m%d_%H%M%S')}.json"
1446
- end
1447
-
1448
- def determine_export_format(filename)
1449
- File.extname(filename) == '.csv' ? :csv : :json
1450
- end
1451
235
  end
1452
236
  end