kdeploy 1.2.33 → 1.2.38

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba776f38e5a34ec7e6dac606aac7202391fdab2e4869aec08f526b3228bc8deb
4
- data.tar.gz: aaa348b87ad83b9c8c1ad3291a2e45395594b922c1594fa82d54cd2a9b7c6d30
3
+ metadata.gz: 29a2ac38f02097f22a48646d3d6483b4e3fd65513f01eb6044ba7efd85f9e51f
4
+ data.tar.gz: 27d6af727a2929a8fe7ec7bca6e7b63a8b2acc6a373ab73baa89b536fbb76941
5
5
  SHA512:
6
- metadata.gz: 7d731fd05fddc4b03c0aaef558e5200b6584772159262405d80cf807eae5f096689201e60aad944f0dd51b2a5e0db7d1d883476ec945cd3984a19383c840830c
7
- data.tar.gz: 6ce986d5b3a60685ed51340298a0d0fbe22d25c2ee7c7cf584b3870d23174d004597e5135c7bb52a2f2f1d1006806c552d49ddcdb92080808348a580ec865947
6
+ metadata.gz: 6ec34aac0c945e25101d9a3d91b68121660a5ab16d47cb7d8cb736805d83ef4f09fd92701351051449ecf6427f85f955a93384ef99cffebebeae09cf1b9a11e1
7
+ data.tar.gz: 416f3adfc65db0a5534cafc2853296fe585742470a482802ad08156f13acb1259ff86bf33c5a10c6c5d671eaae2b04b5a8de3c1f2bdfb0e82646a58bc3512cfd
data/README.md CHANGED
@@ -529,6 +529,67 @@ sync "./config", "/etc/app",
529
529
  - 同步静态资源文件
530
530
  - 保持本地和远程目录结构一致
531
531
 
532
+ ### Chef 风格资源 DSL
533
+
534
+ Kdeploy 提供类似 Chef 的声明式资源 DSL,可替代或与底层原语(`run`、`upload`、`upload_template`)混用。
535
+
536
+ #### `package` - 安装系统包
537
+
538
+ ```ruby
539
+ package "nginx"
540
+ package "nginx", version: "1.18"
541
+ package "nginx", platform: :yum # CentOS/RHEL
542
+ ```
543
+
544
+ 默认使用 apt(Ubuntu/Debian);`platform: :yum` 生成 yum 命令。
545
+
546
+ #### `service` - 管理系统服务(systemd)
547
+
548
+ ```ruby
549
+ service "nginx", action: [:enable, :start]
550
+ service "nginx", action: :restart
551
+ service "nginx", action: [:stop, :disable]
552
+ ```
553
+
554
+ 支持 `:start`、`:stop`、`:restart`、`:reload`、`:enable`、`:disable`。
555
+
556
+ #### `template` - 部署 ERB 模板
557
+
558
+ ```ruby
559
+ template "/etc/nginx/nginx.conf", source: "./config/nginx.conf.erb", variables: { port: 3000 }
560
+ # 或 block 语法
561
+ template "/etc/app.conf" do
562
+ source "./config/app.erb"
563
+ variables(domain: "example.com")
564
+ end
565
+ ```
566
+
567
+ #### `file` - 上传本地文件
568
+
569
+ ```ruby
570
+ file "/etc/nginx/conf.d/app.conf", source: "./config/app.conf"
571
+ ```
572
+
573
+ #### `directory` - 确保远程目录存在
574
+
575
+ ```ruby
576
+ directory "/etc/nginx/conf.d"
577
+ directory "/var/log/app", mode: "0755"
578
+ ```
579
+
580
+ **示例:使用资源 DSL 部署 Nginx**
581
+
582
+ ```ruby
583
+ task :deploy_nginx, roles: :web do
584
+ package "nginx"
585
+ directory "/etc/nginx/conf.d"
586
+ template "/etc/nginx/nginx.conf", source: "./config/nginx.conf.erb", variables: { port: 3000 }
587
+ file "/etc/nginx/conf.d/app.conf", source: "./config/app.conf"
588
+ run "nginx -t"
589
+ service "nginx", action: [:enable, :restart]
590
+ end
591
+ ```
592
+
532
593
  ### 模板支持
533
594
 
534
595
  Kdeploy 支持 ERB(嵌入式 Ruby)模板,用于动态配置生成。
@@ -875,7 +936,6 @@ kdeploy execute deploy.rb deploy --parallel 3
875
936
  - **Executor** (`executor.rb`): SSH/SCP 执行引擎
876
937
  - **Runner** (`runner.rb`): 并发任务执行协调器
877
938
  - **CommandExecutor** (`command_executor.rb`): 单个命令执行
878
- - **CommandGrouper** (`command_grouper.rb`): 命令分组逻辑
879
939
  - **Template** (`template.rb`): ERB 模板渲染
880
940
  - **Output** (`output.rb`): 输出格式化和显示
881
941
  - **Configuration** (`configuration.rb`): 配置管理
@@ -885,10 +945,9 @@ kdeploy execute deploy.rb deploy --parallel 3
885
945
 
886
946
  1. **解析配置**: 加载并解析 `deploy.rb`
887
947
  2. **解析主机**: 根据任务定义确定目标主机
888
- 3. **分组命令**: 按类型分组命令以提高执行效率
889
- 4. **并发执行**: 跨主机并行运行任务
890
- 5. **收集结果**: 收集执行结果和状态
891
- 6. **显示输出**: 格式化并向用户显示结果
948
+ 3. **并发执行**: 跨主机并行运行任务,按序执行每台主机上的命令
949
+ 4. **收集结果**: 收集执行结果和状态
950
+ 5. **显示输出**: 格式化并向用户显示结果
892
951
 
893
952
  ### 并发模型
894
953
 
@@ -928,7 +987,6 @@ kdeploy/
928
987
  │ ├── executor.rb # SSH/SCP 执行器
929
988
  │ ├── runner.rb # 任务运行器
930
989
  │ ├── command_executor.rb # 命令执行器
931
- │ ├── command_grouper.rb # 命令分组器
932
990
  │ ├── template.rb # 模板处理器
933
991
  │ ├── output.rb # 输出接口
934
992
  │ ├── configuration.rb # 配置
data/README_EN.md CHANGED
@@ -486,6 +486,67 @@ upload_template "./config/nginx.conf.erb", "/etc/nginx/nginx.conf",
486
486
  - `destination`: Remote file path
487
487
  - `variables`: Hash of variables for template rendering
488
488
 
489
+ ### Chef-Style Resource DSL
490
+
491
+ Kdeploy provides a declarative resource DSL similar to Chef, which can replace or mix with low-level primitives (`run`, `upload`, `upload_template`).
492
+
493
+ #### `package` - Install System Packages
494
+
495
+ ```ruby
496
+ package "nginx"
497
+ package "nginx", version: "1.18"
498
+ package "nginx", platform: :yum # CentOS/RHEL
499
+ ```
500
+
501
+ Uses apt (Ubuntu/Debian) by default; `platform: :yum` generates yum commands.
502
+
503
+ #### `service` - Manage System Services (systemd)
504
+
505
+ ```ruby
506
+ service "nginx", action: [:enable, :start]
507
+ service "nginx", action: :restart
508
+ service "nginx", action: [:stop, :disable]
509
+ ```
510
+
511
+ Supports `:start`, `:stop`, `:restart`, `:reload`, `:enable`, `:disable`.
512
+
513
+ #### `template` - Deploy ERB Templates
514
+
515
+ ```ruby
516
+ template "/etc/nginx/nginx.conf", source: "./config/nginx.conf.erb", variables: { port: 3000 }
517
+ # Or block syntax
518
+ template "/etc/app.conf" do
519
+ source "./config/app.erb"
520
+ variables(domain: "example.com")
521
+ end
522
+ ```
523
+
524
+ #### `file` - Upload Local Files
525
+
526
+ ```ruby
527
+ file "/etc/nginx/conf.d/app.conf", source: "./config/app.conf"
528
+ ```
529
+
530
+ #### `directory` - Ensure Remote Directory Exists
531
+
532
+ ```ruby
533
+ directory "/etc/nginx/conf.d"
534
+ directory "/var/log/app", mode: "0755"
535
+ ```
536
+
537
+ **Example: Deploy Nginx Using Resource DSL**
538
+
539
+ ```ruby
540
+ task :deploy_nginx, roles: :web do
541
+ package "nginx"
542
+ directory "/etc/nginx/conf.d"
543
+ template "/etc/nginx/nginx.conf", source: "./config/nginx.conf.erb", variables: { port: 3000 }
544
+ file "/etc/nginx/conf.d/app.conf", source: "./config/app.conf"
545
+ run "nginx -t"
546
+ service "nginx", action: [:enable, :restart]
547
+ end
548
+ ```
549
+
489
550
  ### Template Support
490
551
 
491
552
  Kdeploy supports ERB (Embedded Ruby) templates for dynamic configuration generation.
@@ -883,7 +944,6 @@ Enable verbose output by checking the execution output. Kdeploy provides detaile
883
944
  - **Executor** (`executor.rb`): SSH/SCP execution engine
884
945
  - **Runner** (`runner.rb`): Concurrent task execution coordinator
885
946
  - **CommandExecutor** (`command_executor.rb`): Individual command execution
886
- - **CommandGrouper** (`command_grouper.rb`): Command grouping logic
887
947
  - **Template** (`template.rb`): ERB template rendering
888
948
  - **Output** (`output.rb`): Output formatting and display
889
949
  - **Configuration** (`configuration.rb`): Configuration management
@@ -893,10 +953,9 @@ Enable verbose output by checking the execution output. Kdeploy provides detaile
893
953
 
894
954
  1. **Parse Configuration**: Load and parse `deploy.rb`
895
955
  2. **Resolve Hosts**: Determine target hosts based on task definition
896
- 3. **Group Commands**: Group commands by type for efficient execution
897
- 4. **Execute Concurrently**: Run tasks in parallel across hosts
898
- 5. **Collect Results**: Gather execution results and status
899
- 6. **Display Output**: Format and display results to user
956
+ 3. **Execute Concurrently**: Run tasks in parallel across hosts, executing commands in order per host
957
+ 4. **Collect Results**: Gather execution results and status
958
+ 5. **Display Output**: Format and display results to user
900
959
 
901
960
  ### Concurrency Model
902
961
 
@@ -936,7 +995,6 @@ kdeploy/
936
995
  │ ├── executor.rb # SSH/SCP executor
937
996
  │ ├── runner.rb # Task runner
938
997
  │ ├── command_executor.rb # Command executor
939
- │ ├── command_grouper.rb # Command grouper
940
998
  │ ├── template.rb # Template handler
941
999
  │ ├── output.rb # Output interface
942
1000
  │ ├── configuration.rb # Configuration
data/lib/kdeploy/cli.rb CHANGED
@@ -142,21 +142,24 @@ module Kdeploy
142
142
  print_summary(results, formatter) if show_summary
143
143
  end
144
144
 
145
- def print_host_result(_host, result, formatter)
145
+ def print_host_result(host, result, formatter)
146
146
  if %i[success changed].include?(result[:status])
147
- print_success_result(result, formatter)
147
+ print_success_result(host, result, formatter)
148
148
  else
149
- print_failure_result(result, formatter)
149
+ print_failure_result(host, result, formatter)
150
150
  end
151
+
152
+ duration = formatter.calculate_host_duration(result)
153
+ puts "#{formatter.host_prefix(host)}#{formatter.format_host_completed(duration)}" if duration.positive?
151
154
  end
152
155
 
153
- def print_success_result(result, formatter)
156
+ def print_success_result(host, result, formatter)
154
157
  shown = {}
155
158
  grouped = group_output_by_type(result[:output])
156
159
 
157
160
  grouped.each do |type, steps|
158
161
  output_lines = format_steps_by_type(type, steps, shown, formatter)
159
- output_lines.each { |line| puts line }
162
+ output_lines.each { |line| puts "#{formatter.host_prefix(host)}#{line}" }
160
163
  end
161
164
  end
162
165
 
@@ -179,9 +182,21 @@ module Kdeploy
179
182
  end
180
183
  end
181
184
 
182
- def print_failure_result(result, formatter)
185
+ def print_failure_result(host, result, formatter)
183
186
  error_message = extract_error_message(result)
184
- puts formatter.format_error(error_message)
187
+ puts "#{formatter.host_prefix(host)}#{formatter.format_error(error_message)}"
188
+
189
+ # On failure, show steps that were executed (and any captured output)
190
+ # to make troubleshooting easier, even if --debug is not enabled.
191
+ return unless result[:output].is_a?(Array) && result[:output].any?
192
+
193
+ debug_formatter = OutputFormatter.new(debug: true)
194
+ shown = {}
195
+ grouped = group_output_by_type(result[:output])
196
+ grouped.each do |type, steps|
197
+ output_lines = format_steps_by_type(type, steps, shown, debug_formatter)
198
+ output_lines.each { |line| puts "#{debug_formatter.host_prefix(host)}#{line}" }
199
+ end
185
200
  end
186
201
 
187
202
  def print_summary(results, formatter)
@@ -214,6 +229,9 @@ module Kdeploy
214
229
  task_results = execute_single_task(task)
215
230
  # Collect results for final summary
216
231
  all_results[task] = task_results if task_results
232
+
233
+ # Stop executing remaining tasks once any host failed for this task.
234
+ break if task_failed?(task_results)
217
235
  end
218
236
 
219
237
  # Show combined summary at the end for all tasks
@@ -275,6 +293,12 @@ module Kdeploy
275
293
  end
276
294
  end
277
295
 
296
+ def task_failed?(task_results)
297
+ return false unless task_results.is_a?(Hash)
298
+
299
+ task_results.values.any? { |result| result[:status] == :failed }
300
+ end
301
+
278
302
  def print_all_tasks_summary(all_results)
279
303
  debug_mode = options[:debug] || false
280
304
  formatter = OutputFormatter.new(debug: debug_mode)
@@ -11,26 +11,18 @@ module Kdeploy
11
11
  @retry_delay = retry_delay.to_f
12
12
  end
13
13
 
14
- def execute_run(command, host_name)
14
+ def execute_run(command, _host_name)
15
15
  cmd = command[:command]
16
16
  use_sudo = command[:sudo]
17
- show_command_header(host_name, :run, cmd)
18
-
19
- # Show progress indicator for long-running commands
20
- pastel = @output.respond_to?(:pastel) ? @output.pastel : Pastel.new
21
17
 
22
18
  result, duration = measure_time do
23
19
  with_retries { @executor.execute(cmd, use_sudo: use_sudo) }
24
20
  end
25
21
 
26
- # Show execution time if command took more than 1 second
27
- @output.write_line(pastel.dim(" [completed in #{format('%.2f', duration)}s]")) if duration > 1.0
28
-
29
22
  { command: cmd, output: result, duration: duration, type: :run }
30
23
  end
31
24
 
32
- def execute_upload(command, host_name)
33
- show_command_header(host_name, :upload, "#{command[:source]} -> #{command[:destination]}")
25
+ def execute_upload(command, _host_name)
34
26
  _result, duration = measure_time do
35
27
  with_retries { @executor.upload(command[:source], command[:destination]) }
36
28
  end
@@ -41,8 +33,7 @@ module Kdeploy
41
33
  }
42
34
  end
43
35
 
44
- def execute_upload_template(command, host_name)
45
- show_command_header(host_name, :upload_template, "#{command[:source]} -> #{command[:destination]}")
36
+ def execute_upload_template(command, _host_name)
46
37
  _result, duration = measure_time do
47
38
  with_retries do
48
39
  @executor.upload_template(command[:source], command[:destination], command[:variables])
@@ -55,11 +46,9 @@ module Kdeploy
55
46
  }
56
47
  end
57
48
 
58
- def execute_sync(command, host_name)
49
+ def execute_sync(command, _host_name)
59
50
  source = command[:source]
60
51
  destination = command[:destination]
61
- description = build_sync_description(source, destination, command[:delete])
62
- show_command_header(host_name, :sync, description)
63
52
 
64
53
  result, duration = measure_time do
65
54
  with_retries do
@@ -78,12 +67,6 @@ module Kdeploy
78
67
 
79
68
  private
80
69
 
81
- def build_sync_description(source, destination, delete)
82
- desc = "sync: #{source} -> #{destination}"
83
- desc += " (delete: #{delete})" if delete
84
- desc
85
- end
86
-
87
70
  def build_sync_result(source, destination, result, duration)
88
71
  {
89
72
  command: "sync: #{source} -> #{destination}",
@@ -115,32 +98,5 @@ module Kdeploy
115
98
  retry
116
99
  end
117
100
  end
118
-
119
- def show_command_header(host_name, type, description)
120
- # Don't show command header during execution - it will be shown in results
121
- # This reduces noise during execution
122
- end
123
-
124
- def pastel_instance
125
- @output.respond_to?(:pastel) ? @output.pastel : Pastel.new
126
- end
127
-
128
- def format_command_by_type(type, description, pastel)
129
- case type
130
- when :run
131
- format_run_command(description, pastel)
132
- when :upload
133
- @output.write_line(pastel.green(" [upload] #{description}"))
134
- when :upload_template
135
- @output.write_line(pastel.yellow(" [template] #{description}"))
136
- end
137
- end
138
-
139
- def format_run_command(description, pastel)
140
- @output.write_line(pastel.cyan(" [run] #{description.lines.first.strip}"))
141
- description.lines[1..].each do |line|
142
- @output.write_line(" > #{line.strip}") unless line.strip.empty?
143
- end
144
- end
145
101
  end
146
102
  end
data/lib/kdeploy/dsl.rb CHANGED
@@ -1,8 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'set'
4
+ require 'shellwords'
4
5
 
5
6
  module Kdeploy
7
+ # Helper for template resource block: captures source and variables
8
+ class TemplateOptions
9
+ def initialize
10
+ @source = nil
11
+ @variables = {}
12
+ end
13
+
14
+ def variables(val = nil)
15
+ @variables = val if val
16
+ @variables
17
+ end
18
+
19
+ def source(val = nil)
20
+ @source = val if val
21
+ @source
22
+ end
23
+ end
24
+
6
25
  # Domain-specific language for defining hosts, roles, and tasks
7
26
  module DSL
8
27
  def self.included(base)
@@ -154,6 +173,60 @@ module Kdeploy
154
173
  }
155
174
  end
156
175
 
176
+ # -------------------------------------------------------------------------
177
+ # Chef-style resource DSL (compiles to run/upload/upload_template)
178
+ # -------------------------------------------------------------------------
179
+
180
+ # 安装系统包。默认 apt 平台;支持 platform: :yum。
181
+ def package(name, version: nil, platform: :apt)
182
+ @kdeploy_commands ||= []
183
+ cmd = build_package_command(name, version, platform)
184
+ @kdeploy_commands << { type: :run, command: cmd, sudo: true }
185
+ end
186
+
187
+ # 管理系统服务(systemd)。action 支持 :start, :stop, :restart, :reload, :enable, :disable。
188
+ def service(name, action: :start)
189
+ @kdeploy_commands ||= []
190
+ actions = Array(action)
191
+ actions.each do |a|
192
+ cmd = "systemctl #{a} #{Shellwords.escape(name.to_s)}"
193
+ @kdeploy_commands << { type: :run, command: cmd, sudo: true }
194
+ end
195
+ end
196
+
197
+ # 部署 ERB 模板到远程路径。支持 block 或关键字参数。
198
+ def template(destination, source: nil, variables: nil, &block)
199
+ @kdeploy_commands ||= []
200
+ if block
201
+ opts = TemplateOptions.new
202
+ opts.instance_eval(&block)
203
+ src = opts.source || raise(ArgumentError, 'template requires source')
204
+ vars = opts.variables || {}
205
+ else
206
+ raise ArgumentError, 'template requires source' unless source
207
+
208
+ src = source
209
+ vars = variables || {}
210
+ end
211
+ upload_template(src, destination, vars)
212
+ end
213
+
214
+ # 上传本地文件到远程路径。
215
+ def file(destination, source:)
216
+ @kdeploy_commands ||= []
217
+ upload(source, destination)
218
+ end
219
+
220
+ # 确保远程目录存在。支持 mode 参数。
221
+ def directory(path, mode: nil)
222
+ @kdeploy_commands ||= []
223
+ cmd = "mkdir -p #{Shellwords.escape(path.to_s)}"
224
+ @kdeploy_commands << { type: :run, command: cmd, sudo: true }
225
+ return unless mode
226
+
227
+ @kdeploy_commands << { type: :run, command: "chmod #{mode} #{Shellwords.escape(path.to_s)}", sudo: true }
228
+ end
229
+
157
230
  def inventory(&block)
158
231
  instance_eval(&block) if block_given?
159
232
  end
@@ -188,5 +261,20 @@ module Kdeploy
188
261
  end
189
262
  end
190
263
  end
264
+
265
+ def build_package_command(name, version, platform)
266
+ n = Shellwords.escape(name.to_s)
267
+ case platform.to_sym
268
+ when :yum, :rpm
269
+ if version
270
+ "yum install -y #{n}-#{Shellwords.escape(version.to_s)}"
271
+ else
272
+ "yum install -y #{n}"
273
+ end
274
+ else
275
+ base = "apt-get update && apt-get install -y #{n}"
276
+ version ? "#{base}=#{Shellwords.escape(version.to_s)}" : base
277
+ end
278
+ end
191
279
  end
192
280
  end
@@ -20,12 +20,16 @@ module Kdeploy
20
20
 
21
21
  # Raised when SSH operation fails
22
22
  class SSHError < Error
23
- def initialize(message, original_error = nil)
23
+ def initialize(message, original_error = nil, command: nil, exit_status: nil, stdout: nil, stderr: nil)
24
24
  super("SSH operation failed: #{message}")
25
25
  @original_error = original_error
26
+ @command = command
27
+ @exit_status = exit_status
28
+ @stdout = stdout
29
+ @stderr = stderr
26
30
  end
27
31
 
28
- attr_reader :original_error
32
+ attr_reader :original_error, :command, :exit_status, :stdout, :stderr
29
33
  end
30
34
 
31
35
  # Raised when SCP operation fails
@@ -38,16 +38,21 @@ module Kdeploy
38
38
  def execute_command_on_ssh(ssh, command)
39
39
  stdout = String.new
40
40
  stderr = String.new
41
+ exit_status = nil
41
42
 
42
43
  ssh.open_channel do |channel|
43
44
  channel.exec(command) do |_ch, success|
44
45
  raise SSHError, "Could not execute command: #{command}" unless success
45
46
 
46
47
  setup_channel_handlers(channel, stdout, stderr)
48
+ channel.on_request('exit-status') do |_ch, data|
49
+ exit_status = data.read_long
50
+ end
47
51
  end
48
52
  end
49
53
  ssh.loop
50
- build_command_result(stdout, stderr, command)
54
+ raise_nonzero_exit!(command, exit_status, stdout, stderr)
55
+ build_command_result(stdout, stderr, command, exit_status)
51
56
  end
52
57
 
53
58
  def setup_channel_handlers(channel, stdout, stderr)
@@ -60,14 +65,29 @@ module Kdeploy
60
65
  end
61
66
  end
62
67
 
63
- def build_command_result(stdout, stderr, command)
68
+ def build_command_result(stdout, stderr, command, exit_status)
64
69
  {
65
70
  stdout: stdout.strip,
66
71
  stderr: stderr.strip,
67
- command: command
72
+ command: command,
73
+ exit_status: exit_status
68
74
  }
69
75
  end
70
76
 
77
+ def raise_nonzero_exit!(command, exit_status, stdout, stderr)
78
+ return if exit_status.nil?
79
+ return if exit_status.zero?
80
+
81
+ raise SSHError.new(
82
+ "Command exited with status #{exit_status}",
83
+ nil,
84
+ command: command,
85
+ exit_status: exit_status,
86
+ stdout: stdout.strip,
87
+ stderr: stderr.strip
88
+ )
89
+ end
90
+
71
91
  def upload(source, destination, use_sudo: nil)
72
92
  use_sudo = @use_sudo if use_sudo.nil?
73
93
 
@@ -39,27 +39,21 @@ module Kdeploy
39
39
  role :web, %w[web01 web02]
40
40
  role :db, %w[db01]
41
41
 
42
- # Define deployment task for web servers
42
+ # Define deployment task for web servers (Chef-style resource DSL)
43
43
  task :deploy_web, roles: :web do
44
- # Example: Using sudo option for specific command
45
- # run "systemctl stop nginx", sudo: true
46
- run <<~SHELL
47
- sudo systemctl stop nginx
48
- echo "Deploying..."
49
- SHELL
50
-
51
- upload_template './config/nginx.conf.erb', '/etc/nginx/nginx.conf',
52
- domain_name: 'example.com',
53
- port: 3000,
54
- worker_processes: 4,
55
- worker_connections: 2048
56
-
57
- upload './config/app.conf', '/etc/nginx/conf.d/app.conf'
58
-
59
- run <<~SHELL
60
- sudo systemctl start nginx
61
- sudo systemctl status nginx
62
- SHELL
44
+ package 'nginx'
45
+ directory '/etc/nginx/conf.d'
46
+ template '/etc/nginx/nginx.conf',
47
+ source: './config/nginx.conf.erb',
48
+ variables: {
49
+ domain_name: 'example.com',
50
+ port: 3000,
51
+ worker_processes: 4,
52
+ worker_connections: 2048
53
+ }
54
+ file '/etc/nginx/conf.d/app.conf', source: './config/app.conf'
55
+ run 'nginx -t', sudo: true
56
+ service 'nginx', action: %i[enable restart]
63
57
  end
64
58
 
65
59
  # Define backup task for database servers
@@ -71,13 +65,11 @@ module Kdeploy
71
65
  SHELL
72
66
  end
73
67
 
74
- # Define task for specific hosts
68
+ # Define task for specific hosts (resource DSL example)
75
69
  task :maintenance, on: %w[web01] do
76
- run <<~SHELL
77
- sudo systemctl stop nginx
78
- sudo apt-get update && sudo apt-get upgrade -y
79
- sudo systemctl start nginx
80
- SHELL
70
+ service 'nginx', action: :stop
71
+ run 'apt-get update && apt-get upgrade -y', sudo: true
72
+ service 'nginx', action: %i[start enable]
81
73
  end
82
74
 
83
75
  # Define task for all hosts
@@ -199,12 +191,12 @@ module Kdeploy
199
191
  server_name <%= domain_name %>;
200
192
  ```
201
193
 
202
- Variables are passed when uploading the template:
194
+ Variables are passed when uploading the template (Chef-style resource DSL):
203
195
 
204
196
  ```ruby
205
- upload_template "./config/nginx.conf.erb", "/etc/nginx/nginx.conf",
206
- domain_name: "example.com",
207
- worker_processes: 4
197
+ template "/etc/nginx/nginx.conf",
198
+ source: "./config/nginx.conf.erb",
199
+ variables: { domain_name: "example.com", worker_processes: 4 }
208
200
  ```
209
201
 
210
202
  ## 🔐 Using Sudo
@@ -24,64 +24,60 @@ module Kdeploy
24
24
  @pastel.bright_white(" #{host.ljust(20)} #{status_str}")
25
25
  end
26
26
 
27
- def format_upload_steps(steps, shown)
28
- format_file_steps(steps, shown, :upload, @pastel.green(' === Upload ==='), 'upload: ')
27
+ # Prefix for per-step lines to make multi-host logs easier to scan.
28
+ def host_prefix(host)
29
+ @pastel.dim(" #{host.ljust(20)} ")
29
30
  end
30
31
 
31
- def format_template_steps(steps, shown)
32
- format_file_steps(steps, shown, :upload_template, @pastel.yellow(' === Template ==='), 'upload_template: ')
32
+ def format_host_completed(duration)
33
+ @pastel.dim(" [completed in #{format('%.2f', duration)}s]")
33
34
  end
34
35
 
35
- def format_sync_steps(steps, shown)
36
- output = []
37
- steps.each do |step|
38
- next if step_already_shown?(step, :sync, shown)
36
+ def calculate_host_duration(result)
37
+ return 0.0 unless result.is_a?(Hash)
39
38
 
40
- mark_step_as_shown(step, :sync, shown)
41
- output << format_sync_step(step)
42
- end
43
- output
39
+ Array(result[:output]).sum { |step| step[:duration].to_f }
44
40
  end
45
41
 
46
- def format_file_steps(steps, shown, type, _header, prefix)
47
- output = []
48
- steps.each do |step|
49
- next if step_already_shown?(step, type, shown)
42
+ def format_upload_steps(steps, _shown = nil)
43
+ format_file_steps(steps, :upload, 'upload: ')
44
+ end
50
45
 
51
- mark_step_as_shown(step, type, shown)
52
- output << format_file_step(step, type, prefix)
53
- end
54
- output
46
+ def format_template_steps(steps, _shown = nil)
47
+ format_file_steps(steps, :upload_template, 'upload_template: ')
48
+ end
49
+
50
+ def format_sync_steps(steps, _shown = nil)
51
+ steps.map { |step| format_sync_step(step) }
52
+ end
53
+
54
+ def format_file_steps(steps, type, prefix)
55
+ steps.map { |step| format_file_step(step, type, prefix) }
55
56
  end
56
57
 
57
58
  def format_file_step(step, type, prefix)
58
59
  duration_str = format_duration(step[:duration])
60
+ status_str = format_step_status(step)
59
61
  icon = type == :upload ? '📤' : '📝'
60
62
  file_path = step[:command].sub(prefix, '')
61
63
  # Truncate long paths for cleaner output
62
64
  display_path = file_path.length > 50 ? "...#{file_path[-47..]}" : file_path
63
65
  color_method = type == :upload ? :green : :yellow
64
- @pastel.dim(" #{icon} ") + @pastel.send(color_method, display_path) + duration_str
66
+ @pastel.dim(" #{icon} ") + @pastel.send(color_method, display_path) + duration_str + " #{status_str}"
65
67
  end
66
68
 
67
- def format_run_steps(steps, shown)
68
- output = []
69
- steps.each do |step|
70
- next if step_already_shown?(step, :run, shown)
71
-
72
- mark_step_as_shown(step, :run, shown)
73
- output.concat(format_single_run_step(step))
74
- end
75
- output
69
+ def format_run_steps(steps, _shown = nil)
70
+ steps.flat_map { |step| format_single_run_step(step) }
76
71
  end
77
72
 
78
73
  def format_single_run_step(step)
79
74
  output = []
80
75
  duration_str = format_duration(step[:duration])
81
- command_line = step[:command].to_s.lines.first.strip
76
+ status_str = format_step_status(step)
77
+ command_line = first_meaningful_command_line(step[:command].to_s)
82
78
  # Truncate long commands for cleaner output
83
79
  display_cmd = command_line.length > 60 ? "#{command_line[0..57]}..." : command_line
84
- output << (@pastel.dim(' • ') + @pastel.cyan(display_cmd) + duration_str)
80
+ output << (@pastel.dim(' • ') + @pastel.cyan(display_cmd) + duration_str + " #{status_str}")
85
81
  # Only show multiline details in debug mode
86
82
  if @debug
87
83
  output.concat(format_multiline_command(step[:command]))
@@ -187,6 +183,17 @@ module Kdeploy
187
183
  output
188
184
  end
189
185
 
186
+ def first_meaningful_command_line(command)
187
+ lines = command.to_s.lines.map(&:strip)
188
+ lines.each do |line|
189
+ next if line.empty?
190
+ next if line.start_with?('#')
191
+
192
+ return line
193
+ end
194
+ lines.first.to_s
195
+ end
196
+
190
197
  def format_command_output(output)
191
198
  result = []
192
199
  return result unless output
@@ -240,18 +247,9 @@ module Kdeploy
240
247
  result
241
248
  end
242
249
 
243
- def step_already_shown?(step, type, shown)
244
- key = [step[:command], type].hash
245
- shown[key]
246
- end
247
-
248
- def mark_step_as_shown(step, type, shown)
249
- key = [step[:command], type].hash
250
- shown[key] = true
251
- end
252
-
253
250
  def format_sync_step(step)
254
251
  duration_str = format_duration(step[:duration])
252
+ status_str = format_step_status(step)
255
253
  sync_path = step[:command].sub('sync: ', '')
256
254
  # Truncate long paths for cleaner output
257
255
  display_path = sync_path.length > 50 ? "...#{sync_path[-47..]}" : sync_path
@@ -266,7 +264,15 @@ module Kdeploy
266
264
  stats << @pastel.yellow("#{deleted} deleted") if deleted.positive?
267
265
  stats_str = stats.any? ? " (#{stats.join(', ')})" : " (#{total} files)"
268
266
 
269
- @pastel.dim(' 📁 ') + @pastel.cyan(display_path) + @pastel.dim(stats_str) + duration_str
267
+ @pastel.dim(' 📁 ') + @pastel.cyan(display_path) + @pastel.dim(stats_str) + duration_str + " #{status_str}"
268
+ end
269
+
270
+ def format_step_status(step)
271
+ if step.is_a?(Hash) && step.key?(:error) && step[:error] && !step[:error].to_s.empty?
272
+ @pastel.red('✗ failed')
273
+ else
274
+ @pastel.green('✓ ok')
275
+ end
270
276
  end
271
277
  end
272
278
  end
@@ -41,10 +41,6 @@ module Kdeploy
41
41
  # If no hosts, return empty results immediately
42
42
  return @results if futures.empty?
43
43
 
44
- # Show progress while waiting for tasks to complete
45
- total = futures.length
46
- completed = 0
47
-
48
44
  # Collect results from futures
49
45
  futures.each_with_index do |future, index|
50
46
  host_name = @host_names[index] # Get host name from the stored list
@@ -86,14 +82,6 @@ module Kdeploy
86
82
  # Ensure we always have a result for this host
87
83
  @results[host_name] ||= { status: :unknown, error: 'No result collected', output: [] }
88
84
  end
89
-
90
- completed += 1
91
- # Show progress for multiple hosts
92
- next unless total > 1
93
-
94
- pastel = @output.respond_to?(:pastel) ? @output.pastel : Pastel.new
95
- @output.write_line(pastel.dim(" [Progress: #{completed}/#{total} hosts completed]"))
96
- @output.flush if @output.respond_to?(:flush)
97
85
  end
98
86
 
99
87
  @results
@@ -125,41 +113,47 @@ module Kdeploy
125
113
  result = { status: :success, output: [] }
126
114
 
127
115
  begin
128
- execute_grouped_commands(task, command_executor, name, result, task_name)
116
+ execute_commands(task, command_executor, name, result, task_name)
129
117
  rescue StandardError => e
130
- # Ensure result is always set, even on error
131
- # Don't re-raise, as it would cause future.value to fail
132
- result = { status: :failed, error: "#{e.class}: #{e.message}", output: [] }
118
+ # Keep any already collected step output for troubleshooting.
119
+ result[:status] = :failed
120
+ result[:error] = "#{e.class}: #{e.message}"
133
121
  end
134
122
 
135
123
  # Return the result so it can be collected from the future
136
124
  [name, result]
137
125
  end
138
126
 
139
- def execute_grouped_commands(task, command_executor, name, result, task_name)
127
+ def execute_commands(task, command_executor, name, result, task_name)
140
128
  commands = task[:block].call
141
- grouped_commands = CommandGrouper.group(commands)
142
129
 
143
- grouped_commands.each_value do |command_group|
144
- execute_command_group(command_group, command_executor, name, result, task_name)
130
+ commands.each do |command|
131
+ step_result = execute_command(command_executor, command, name)
132
+ result[:output] << step_result
133
+ rescue StandardError => e
134
+ step = step_description(command)
135
+ result[:status] = :failed
136
+ result[:error] = "task=#{task_name} host=#{name} step=#{step} error=#{e.class}: #{e.message}"
137
+ result[:output] << {
138
+ type: command[:type],
139
+ command: step_command_string(command),
140
+ duration: 0.0,
141
+ error: "#{e.class}: #{e.message}",
142
+ output: error_output_for_step(e)
143
+ }
144
+ break
145
145
  end
146
146
  end
147
147
 
148
- def execute_command_group(command_group, command_executor, name, result, task_name)
149
- first_cmd = command_group.first
150
- task_desc = CommandGrouper.task_description(first_cmd)
151
- show_task_header(task_desc)
148
+ def error_output_for_step(error)
149
+ return nil unless error.is_a?(Kdeploy::SSHError)
152
150
 
153
- command_group.each_with_index do |command, index|
154
- # Show progress for multiple commands
155
- if command_group.length > 1
156
- pastel = @output.respond_to?(:pastel) ? @output.pastel : Pastel.new
157
- @output.write_line(pastel.dim(" [Step #{index + 1}/#{command_group.length}]"))
158
- end
159
-
160
- step_result = execute_command_with_context(command_executor, command, name, task_name)
161
- result[:output] << step_result
162
- end
151
+ {
152
+ stdout: error.stdout,
153
+ stderr: error.stderr,
154
+ exit_status: error.exit_status,
155
+ command: error.command
156
+ }
163
157
  end
164
158
 
165
159
  def execute_command(command_executor, command, host_name)
@@ -177,11 +171,19 @@ module Kdeploy
177
171
  end
178
172
  end
179
173
 
180
- def execute_command_with_context(command_executor, command, host_name, task_name)
181
- execute_command(command_executor, command, host_name)
182
- rescue StandardError => e
183
- step = step_description(command)
184
- raise StandardError, "task=#{task_name} host=#{host_name} step=#{step} error=#{e.class}: #{e.message}"
174
+ def step_command_string(command)
175
+ case command[:type]
176
+ when :run
177
+ command[:command].to_s
178
+ when :upload
179
+ "upload: #{command[:source]} -> #{command[:destination]}"
180
+ when :upload_template
181
+ "upload_template: #{command[:source]} -> #{command[:destination]}"
182
+ when :sync
183
+ "sync: #{command[:source]} -> #{command[:destination]}"
184
+ else
185
+ command[:type].to_s
186
+ end
185
187
  end
186
188
 
187
189
  def step_description(command)
@@ -199,10 +201,5 @@ module Kdeploy
199
201
  command[:type].to_s
200
202
  end
201
203
  end
202
-
203
- def show_task_header(task_desc)
204
- # Don't show command header during execution - it will be shown in results
205
- # This reduces noise during execution
206
- end
207
204
  end
208
205
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Kdeploy module for version management
4
4
  module Kdeploy
5
- VERSION = '1.2.33' unless const_defined?(:VERSION)
5
+ VERSION = '1.2.38' unless const_defined?(:VERSION)
6
6
  end
data/lib/kdeploy.rb CHANGED
@@ -8,7 +8,6 @@ require_relative 'kdeploy/banner'
8
8
  require_relative 'kdeploy/file_filter'
9
9
  require_relative 'kdeploy/dsl'
10
10
  require_relative 'kdeploy/executor'
11
- require_relative 'kdeploy/command_grouper'
12
11
  require_relative 'kdeploy/command_executor'
13
12
  require_relative 'kdeploy/output_formatter'
14
13
  require_relative 'kdeploy/help_formatter'
data/r.md CHANGED
@@ -0,0 +1 @@
1
+ # Your product documentation
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kdeploy
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.33
4
+ version: 1.2.38
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kk
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-01-25 00:00:00.000000000 Z
11
+ date: 2026-02-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bcrypt_pbkdf
@@ -169,7 +169,6 @@ files:
169
169
  - lib/kdeploy/banner.rb
170
170
  - lib/kdeploy/cli.rb
171
171
  - lib/kdeploy/command_executor.rb
172
- - lib/kdeploy/command_grouper.rb
173
172
  - lib/kdeploy/completions/kdeploy.bash
174
173
  - lib/kdeploy/completions/kdeploy.zsh
175
174
  - lib/kdeploy/configuration.rb
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kdeploy
4
- # Groups commands by type and generates task descriptions
5
- class CommandGrouper
6
- def self.group(commands)
7
- commands.group_by do |cmd|
8
- group_key_for(cmd)
9
- end
10
- end
11
-
12
- def self.group_key_for(cmd)
13
- case cmd[:type]
14
- when :upload, :upload_template, :sync
15
- "#{cmd[:type]}_#{cmd[:source]}"
16
- when :run
17
- "#{cmd[:type]}_#{cmd[:command].to_s.lines.first.strip}"
18
- else
19
- cmd[:type].to_s
20
- end
21
- end
22
-
23
- def self.task_description(command)
24
- case command[:type]
25
- when :upload
26
- "upload #{command[:source]}"
27
- when :upload_template
28
- "template #{command[:source]}"
29
- when :sync
30
- "sync #{command[:source]}"
31
- when :run
32
- command[:command].to_s.lines.first.strip
33
- else
34
- command[:type].to_s
35
- end
36
- end
37
- end
38
- end