kdeploy 1.2.30 → 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 +4 -4
- data/AGENTS.md +18 -0
- data/README.md +78 -6
- data/README_EN.md +78 -6
- data/exe/kdeploy +0 -14
- data/lib/kdeploy/cli.rb +137 -14
- data/lib/kdeploy/command_executor.rb +30 -81
- data/lib/kdeploy/configuration.rb +9 -1
- data/lib/kdeploy/dsl.rb +109 -0
- data/lib/kdeploy/errors.rb +6 -2
- data/lib/kdeploy/executor.rb +23 -3
- data/lib/kdeploy/file_filter.rb +14 -3
- data/lib/kdeploy/help_formatter.rb +12 -1
- data/lib/kdeploy/initializer.rb +22 -30
- data/lib/kdeploy/output_formatter.rb +49 -43
- data/lib/kdeploy/runner.rb +74 -46
- data/lib/kdeploy/version.rb +1 -1
- data/lib/kdeploy.rb +0 -1
- data/r.md +1 -0
- metadata +4 -3
- data/lib/kdeploy/command_grouper.rb +0 -38
|
@@ -24,64 +24,60 @@ module Kdeploy
|
|
|
24
24
|
@pastel.bright_white(" #{host.ljust(20)} #{status_str}")
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
32
|
-
|
|
32
|
+
def format_host_completed(duration)
|
|
33
|
+
@pastel.dim(" [completed in #{format('%.2f', duration)}s]")
|
|
33
34
|
end
|
|
34
35
|
|
|
35
|
-
def
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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,
|
|
68
|
-
|
|
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
|
-
|
|
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
|
data/lib/kdeploy/runner.rb
CHANGED
|
@@ -6,21 +6,23 @@ module Kdeploy
|
|
|
6
6
|
# Concurrent task runner for executing tasks across multiple hosts
|
|
7
7
|
class Runner
|
|
8
8
|
def initialize(hosts, tasks, parallel: Configuration.default_parallel, output: ConsoleOutput.new,
|
|
9
|
-
debug: false, base_dir: nil
|
|
9
|
+
debug: false, base_dir: nil, retries: Configuration.default_retries,
|
|
10
|
+
retry_delay: Configuration.default_retry_delay)
|
|
10
11
|
@hosts = hosts
|
|
11
12
|
@tasks = tasks
|
|
12
13
|
@parallel = parallel
|
|
13
14
|
@output = output
|
|
14
15
|
@debug = debug
|
|
15
16
|
@base_dir = base_dir
|
|
17
|
+
@retries = retries
|
|
18
|
+
@retry_delay = retry_delay
|
|
16
19
|
@pool = Concurrent::FixedThreadPool.new(@parallel)
|
|
17
20
|
@results = Concurrent::Hash.new
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
def run(task_name)
|
|
21
24
|
task = find_task(task_name)
|
|
22
|
-
|
|
23
|
-
results
|
|
25
|
+
execute_concurrent_tasks(task, task_name)
|
|
24
26
|
ensure
|
|
25
27
|
@pool.shutdown
|
|
26
28
|
end
|
|
@@ -33,16 +35,12 @@ module Kdeploy
|
|
|
33
35
|
task
|
|
34
36
|
end
|
|
35
37
|
|
|
36
|
-
def execute_concurrent_tasks(task)
|
|
37
|
-
futures = create_task_futures(task)
|
|
38
|
+
def execute_concurrent_tasks(task, task_name)
|
|
39
|
+
futures = create_task_futures(task, task_name)
|
|
38
40
|
|
|
39
41
|
# If no hosts, return empty results immediately
|
|
40
42
|
return @results if futures.empty?
|
|
41
43
|
|
|
42
|
-
# Show progress while waiting for tasks to complete
|
|
43
|
-
total = futures.length
|
|
44
|
-
completed = 0
|
|
45
|
-
|
|
46
44
|
# Collect results from futures
|
|
47
45
|
futures.each_with_index do |future, index|
|
|
48
46
|
host_name = @host_names[index] # Get host name from the stored list
|
|
@@ -84,74 +82,78 @@ module Kdeploy
|
|
|
84
82
|
# Ensure we always have a result for this host
|
|
85
83
|
@results[host_name] ||= { status: :unknown, error: 'No result collected', output: [] }
|
|
86
84
|
end
|
|
87
|
-
|
|
88
|
-
completed += 1
|
|
89
|
-
# Show progress for multiple hosts
|
|
90
|
-
next unless total > 1
|
|
91
|
-
|
|
92
|
-
pastel = @output.respond_to?(:pastel) ? @output.pastel : Pastel.new
|
|
93
|
-
@output.write_line(pastel.dim(" [Progress: #{completed}/#{total} hosts completed]"))
|
|
94
|
-
@output.flush if @output.respond_to?(:flush)
|
|
95
85
|
end
|
|
96
86
|
|
|
97
87
|
@results
|
|
98
88
|
end
|
|
99
89
|
|
|
100
|
-
def create_task_futures(task)
|
|
90
|
+
def create_task_futures(task, task_name)
|
|
101
91
|
# Store host names in order to match with futures
|
|
102
92
|
@host_names = @hosts.keys
|
|
103
93
|
@hosts.map do |name, config|
|
|
104
94
|
Concurrent::Future.execute(executor: @pool) do
|
|
105
|
-
execute_task_for_host(name, config, task)
|
|
95
|
+
execute_task_for_host(name, config, task, task_name)
|
|
106
96
|
end
|
|
107
97
|
end
|
|
108
98
|
end
|
|
109
99
|
|
|
110
100
|
private
|
|
111
101
|
|
|
112
|
-
def execute_task_for_host(name, config, task)
|
|
102
|
+
def execute_task_for_host(name, config, task, task_name)
|
|
113
103
|
# Add base_dir to config for path resolution
|
|
114
104
|
config_with_base_dir = config.merge(base_dir: @base_dir)
|
|
115
105
|
executor = Executor.new(config_with_base_dir)
|
|
116
|
-
command_executor = CommandExecutor.new(
|
|
106
|
+
command_executor = CommandExecutor.new(
|
|
107
|
+
executor,
|
|
108
|
+
@output,
|
|
109
|
+
debug: @debug,
|
|
110
|
+
retries: @retries,
|
|
111
|
+
retry_delay: @retry_delay
|
|
112
|
+
)
|
|
117
113
|
result = { status: :success, output: [] }
|
|
118
114
|
|
|
119
115
|
begin
|
|
120
|
-
|
|
116
|
+
execute_commands(task, command_executor, name, result, task_name)
|
|
121
117
|
rescue StandardError => e
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
result = {
|
|
118
|
+
# Keep any already collected step output for troubleshooting.
|
|
119
|
+
result[:status] = :failed
|
|
120
|
+
result[:error] = "#{e.class}: #{e.message}"
|
|
125
121
|
end
|
|
126
122
|
|
|
127
123
|
# Return the result so it can be collected from the future
|
|
128
124
|
[name, result]
|
|
129
125
|
end
|
|
130
126
|
|
|
131
|
-
def
|
|
127
|
+
def execute_commands(task, command_executor, name, result, task_name)
|
|
132
128
|
commands = task[:block].call
|
|
133
|
-
grouped_commands = CommandGrouper.group(commands)
|
|
134
129
|
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
137
145
|
end
|
|
138
146
|
end
|
|
139
147
|
|
|
140
|
-
def
|
|
141
|
-
|
|
142
|
-
task_desc = CommandGrouper.task_description(first_cmd)
|
|
143
|
-
show_task_header(task_desc)
|
|
144
|
-
|
|
145
|
-
command_group.each_with_index do |command, index|
|
|
146
|
-
# Show progress for multiple commands
|
|
147
|
-
if command_group.length > 1
|
|
148
|
-
pastel = @output.respond_to?(:pastel) ? @output.pastel : Pastel.new
|
|
149
|
-
@output.write_line(pastel.dim(" [Step #{index + 1}/#{command_group.length}]"))
|
|
150
|
-
end
|
|
148
|
+
def error_output_for_step(error)
|
|
149
|
+
return nil unless error.is_a?(Kdeploy::SSHError)
|
|
151
150
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
{
|
|
152
|
+
stdout: error.stdout,
|
|
153
|
+
stderr: error.stderr,
|
|
154
|
+
exit_status: error.exit_status,
|
|
155
|
+
command: error.command
|
|
156
|
+
}
|
|
155
157
|
end
|
|
156
158
|
|
|
157
159
|
def execute_command(command_executor, command, host_name)
|
|
@@ -169,9 +171,35 @@ module Kdeploy
|
|
|
169
171
|
end
|
|
170
172
|
end
|
|
171
173
|
|
|
172
|
-
def
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def step_description(command)
|
|
190
|
+
case command[:type]
|
|
191
|
+
when :run
|
|
192
|
+
first = command[:command].to_s.lines.first&.strip
|
|
193
|
+
"run: #{first}"
|
|
194
|
+
when :upload
|
|
195
|
+
"upload: #{command[:source]} -> #{command[:destination]}"
|
|
196
|
+
when :upload_template
|
|
197
|
+
"upload_template: #{command[:source]} -> #{command[:destination]}"
|
|
198
|
+
when :sync
|
|
199
|
+
"sync: #{command[:source]} -> #{command[:destination]}"
|
|
200
|
+
else
|
|
201
|
+
command[:type].to_s
|
|
202
|
+
end
|
|
175
203
|
end
|
|
176
204
|
end
|
|
177
205
|
end
|
data/lib/kdeploy/version.rb
CHANGED
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'
|
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.
|
|
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:
|
|
11
|
+
date: 2026-02-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bcrypt_pbkdf
|
|
@@ -160,6 +160,7 @@ extensions:
|
|
|
160
160
|
- ext/mkrf_conf.rb
|
|
161
161
|
extra_rdoc_files: []
|
|
162
162
|
files:
|
|
163
|
+
- AGENTS.md
|
|
163
164
|
- README.md
|
|
164
165
|
- README_EN.md
|
|
165
166
|
- exe/kdeploy
|
|
@@ -168,7 +169,6 @@ files:
|
|
|
168
169
|
- lib/kdeploy/banner.rb
|
|
169
170
|
- lib/kdeploy/cli.rb
|
|
170
171
|
- lib/kdeploy/command_executor.rb
|
|
171
|
-
- lib/kdeploy/command_grouper.rb
|
|
172
172
|
- lib/kdeploy/completions/kdeploy.bash
|
|
173
173
|
- lib/kdeploy/completions/kdeploy.zsh
|
|
174
174
|
- lib/kdeploy/configuration.rb
|
|
@@ -184,6 +184,7 @@ files:
|
|
|
184
184
|
- lib/kdeploy/runner.rb
|
|
185
185
|
- lib/kdeploy/template.rb
|
|
186
186
|
- lib/kdeploy/version.rb
|
|
187
|
+
- r.md
|
|
187
188
|
homepage: https://github.com/kevin197011/kdeploy
|
|
188
189
|
licenses:
|
|
189
190
|
- MIT
|
|
@@ -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
|