kdeploy 1.2.30 → 1.2.33
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 +14 -0
- data/README_EN.md +14 -0
- data/exe/kdeploy +0 -14
- data/lib/kdeploy/cli.rb +106 -7
- data/lib/kdeploy/command_executor.rb +26 -33
- data/lib/kdeploy/configuration.rb +9 -1
- data/lib/kdeploy/dsl.rb +21 -0
- data/lib/kdeploy/file_filter.rb +14 -3
- data/lib/kdeploy/help_formatter.rb +12 -1
- data/lib/kdeploy/runner.rb +46 -15
- data/lib/kdeploy/version.rb +1 -1
- data/r.md +0 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ba776f38e5a34ec7e6dac606aac7202391fdab2e4869aec08f526b3228bc8deb
|
|
4
|
+
data.tar.gz: aaa348b87ad83b9c8c1ad3291a2e45395594b922c1594fa82d54cd2a9b7c6d30
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7d731fd05fddc4b03c0aaef558e5200b6584772159262405d80cf807eae5f096689201e60aad944f0dd51b2a5e0db7d1d883476ec945cd3984a19383c840830c
|
|
7
|
+
data.tar.gz: 6ce986d5b3a60685ed51340298a0d0fbe22d25c2ee7c7cf584b3870d23174d004597e5135c7bb52a2f2f1d1006806c552d49ddcdb92080808348a580ec865947
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!-- OPENSPEC:START -->
|
|
2
|
+
# OpenSpec Instructions
|
|
3
|
+
|
|
4
|
+
These instructions are for AI assistants working in this project.
|
|
5
|
+
|
|
6
|
+
Always open `@/openspec/AGENTS.md` when the request:
|
|
7
|
+
- Mentions planning or proposals (words like proposal, spec, change, plan)
|
|
8
|
+
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
|
|
9
|
+
- Sounds ambiguous and you need the authoritative spec before coding
|
|
10
|
+
|
|
11
|
+
Use `@/openspec/AGENTS.md` to learn:
|
|
12
|
+
- How to create and apply change proposals
|
|
13
|
+
- Spec format and conventions
|
|
14
|
+
- Project structure and guidelines
|
|
15
|
+
|
|
16
|
+
Keep this managed block so 'openspec update' can refresh the instructions.
|
|
17
|
+
|
|
18
|
+
<!-- OPENSPEC:END -->
|
data/README.md
CHANGED
|
@@ -201,6 +201,11 @@ kdeploy execute deploy.rb deploy_web
|
|
|
201
201
|
- `--limit HOSTS`: 限制执行到特定主机(逗号分隔)
|
|
202
202
|
- `--parallel NUM`: 并行执行数量(默认: 10)
|
|
203
203
|
- `--dry-run`: 预览模式 - 显示将要执行的操作而不实际执行
|
|
204
|
+
- `--debug`: 调试模式 - 显示 `run` 命令的 stdout/stderr 详细输出(便于排查问题)
|
|
205
|
+
- `--no-banner`: 不输出 Banner(更适合脚本/CI 场景)
|
|
206
|
+
- `--format FORMAT`: 输出格式(`text`|`json`,默认 `text`)
|
|
207
|
+
- `--retries N`: 网络相关操作重试次数(默认 `0`)
|
|
208
|
+
- `--retry-delay SECONDS`: 每次重试间隔秒数(默认 `1`)
|
|
204
209
|
|
|
205
210
|
**示例:**
|
|
206
211
|
```bash
|
|
@@ -213,6 +218,15 @@ kdeploy execute deploy.rb deploy_web --limit web01,web02
|
|
|
213
218
|
# 使用自定义并行数量
|
|
214
219
|
kdeploy execute deploy.rb deploy_web --parallel 5
|
|
215
220
|
|
|
221
|
+
# 输出详细调试信息(stdout/stderr)
|
|
222
|
+
kdeploy execute deploy.rb deploy_web --debug
|
|
223
|
+
|
|
224
|
+
# 机器可读 JSON 输出(便于集成)
|
|
225
|
+
kdeploy execute deploy.rb deploy_web --format json --no-banner
|
|
226
|
+
|
|
227
|
+
# 重试网络抖动导致的失败
|
|
228
|
+
kdeploy execute deploy.rb deploy_web --retries 3 --retry-delay 1
|
|
229
|
+
|
|
216
230
|
# 组合选项
|
|
217
231
|
kdeploy execute deploy.rb deploy_web --limit web01 --parallel 3 --dry-run
|
|
218
232
|
```
|
data/README_EN.md
CHANGED
|
@@ -200,6 +200,11 @@ kdeploy execute deploy.rb deploy_web
|
|
|
200
200
|
- `--limit HOSTS`: Limit execution to specific hosts (comma-separated)
|
|
201
201
|
- `--parallel NUM`: Number of parallel executions (default: 10)
|
|
202
202
|
- `--dry-run`: Preview mode - show what would be done without executing
|
|
203
|
+
- `--debug`: Debug mode - show detailed stdout/stderr output for `run` steps
|
|
204
|
+
- `--no-banner`: Do not print banner (automation-friendly)
|
|
205
|
+
- `--format FORMAT`: Output format (`text`|`json`, default `text`)
|
|
206
|
+
- `--retries N`: Retry count for network operations (default `0`)
|
|
207
|
+
- `--retry-delay SECONDS`: Delay between retries in seconds (default `1`)
|
|
203
208
|
|
|
204
209
|
**Examples:**
|
|
205
210
|
```bash
|
|
@@ -212,6 +217,15 @@ kdeploy execute deploy.rb deploy_web --limit web01,web02
|
|
|
212
217
|
# Use custom parallel count
|
|
213
218
|
kdeploy execute deploy.rb deploy_web --parallel 5
|
|
214
219
|
|
|
220
|
+
# Show detailed stdout/stderr output
|
|
221
|
+
kdeploy execute deploy.rb deploy_web --debug
|
|
222
|
+
|
|
223
|
+
# Machine-readable JSON output
|
|
224
|
+
kdeploy execute deploy.rb deploy_web --format json --no-banner
|
|
225
|
+
|
|
226
|
+
# Retry transient network failures
|
|
227
|
+
kdeploy execute deploy.rb deploy_web --retries 3 --retry-delay 1
|
|
228
|
+
|
|
215
229
|
# Combine options
|
|
216
230
|
kdeploy execute deploy.rb deploy_web --limit web01 --parallel 3 --dry-run
|
|
217
231
|
```
|
data/exe/kdeploy
CHANGED
|
@@ -1,19 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
# 自动修复常见 gem 扩展
|
|
5
|
-
# %w[debug rbs].zip(%w[1.7.1 2.8.2]).each do |gem_name, version|
|
|
6
|
-
# require gem_name
|
|
7
|
-
# rescue LoadError
|
|
8
|
-
# warn "[Kdeploy] 自动修复 #{gem_name}-#{version} ..."
|
|
9
|
-
# system("gem pristine #{gem_name} --version #{version}")
|
|
10
|
-
# begin
|
|
11
|
-
# require gem_name
|
|
12
|
-
# rescue LoadError
|
|
13
|
-
# warn "[Kdeploy] 依然无法加载 #{gem_name}-#{version},请手动修复。"
|
|
14
|
-
# end
|
|
15
|
-
# end
|
|
16
|
-
|
|
17
4
|
require 'kdeploy'
|
|
18
|
-
|
|
19
5
|
Kdeploy::CLI.start(ARGV)
|
data/lib/kdeploy/cli.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'thor'
|
|
4
|
+
require 'json'
|
|
4
5
|
require 'pastel'
|
|
5
6
|
require 'tty-table'
|
|
6
7
|
require 'tty-box'
|
|
@@ -43,9 +44,13 @@ module Kdeploy
|
|
|
43
44
|
|
|
44
45
|
desc 'execute TASK_FILE [TASK]', 'Execute deployment tasks from file'
|
|
45
46
|
method_option :limit, type: :string, desc: 'Limit to specific hosts (comma-separated)'
|
|
46
|
-
method_option :parallel, type: :numeric,
|
|
47
|
+
method_option :parallel, type: :numeric, desc: 'Number of parallel executions'
|
|
47
48
|
method_option :dry_run, type: :boolean, desc: 'Show what would be done'
|
|
48
49
|
method_option :debug, type: :boolean, desc: 'Show detailed command output (stdout/stderr)'
|
|
50
|
+
method_option :no_banner, type: :boolean, desc: 'Do not print banner'
|
|
51
|
+
method_option :format, type: :string, default: 'text', desc: 'Output format (text|json)'
|
|
52
|
+
method_option :retries, type: :numeric, desc: 'Retry count for network operations (default: 0)'
|
|
53
|
+
method_option :retry_delay, type: :numeric, desc: 'Retry delay seconds (default: 1)'
|
|
49
54
|
def execute(task_file, task_name = nil)
|
|
50
55
|
load_config_file
|
|
51
56
|
show_banner_once
|
|
@@ -53,7 +58,10 @@ module Kdeploy
|
|
|
53
58
|
load_task_file(task_file)
|
|
54
59
|
|
|
55
60
|
tasks_to_run = determine_tasks(task_name)
|
|
56
|
-
execute_tasks(tasks_to_run)
|
|
61
|
+
all_results = execute_tasks(tasks_to_run)
|
|
62
|
+
|
|
63
|
+
# Exit non-zero if any executed host failed.
|
|
64
|
+
exit 1 if any_failed?(all_results)
|
|
57
65
|
rescue StandardError => e
|
|
58
66
|
puts Kdeploy::Banner.show_error(e.message)
|
|
59
67
|
exit 1
|
|
@@ -189,6 +197,7 @@ module Kdeploy
|
|
|
189
197
|
def show_banner_once
|
|
190
198
|
@banner_printed ||= false
|
|
191
199
|
return if @banner_printed
|
|
200
|
+
return if options[:no_banner]
|
|
192
201
|
|
|
193
202
|
puts Kdeploy::Banner.show
|
|
194
203
|
@banner_printed = true
|
|
@@ -208,7 +217,9 @@ module Kdeploy
|
|
|
208
217
|
end
|
|
209
218
|
|
|
210
219
|
# Show combined summary at the end for all tasks
|
|
211
|
-
print_all_tasks_summary(all_results) unless all_results.empty?
|
|
220
|
+
print_all_tasks_summary(all_results) unless all_results.empty? || options[:format] == 'json'
|
|
221
|
+
|
|
222
|
+
all_results
|
|
212
223
|
end
|
|
213
224
|
|
|
214
225
|
def execute_single_task(task)
|
|
@@ -221,7 +232,11 @@ module Kdeploy
|
|
|
221
232
|
end
|
|
222
233
|
|
|
223
234
|
if options[:dry_run]
|
|
224
|
-
|
|
235
|
+
if options[:format] == 'json'
|
|
236
|
+
print_dry_run_json(hosts, task)
|
|
237
|
+
else
|
|
238
|
+
print_dry_run(hosts, task)
|
|
239
|
+
end
|
|
225
240
|
return nil
|
|
226
241
|
end
|
|
227
242
|
|
|
@@ -232,20 +247,34 @@ module Kdeploy
|
|
|
232
247
|
output = ConsoleOutput.new
|
|
233
248
|
parallel_count = options[:parallel] || Configuration.default_parallel
|
|
234
249
|
debug_mode = options[:debug] || false
|
|
250
|
+
retries = options[:retries].nil? ? Configuration.default_retries : options[:retries]
|
|
251
|
+
retry_delay = options[:retry_delay].nil? ? Configuration.default_retry_delay : options[:retry_delay]
|
|
235
252
|
base_dir = @task_file_dir
|
|
236
253
|
runner = Runner.new(
|
|
237
254
|
hosts, self.class.kdeploy_tasks,
|
|
238
255
|
parallel: parallel_count,
|
|
239
256
|
output: output,
|
|
240
257
|
debug: debug_mode,
|
|
241
|
-
base_dir: base_dir
|
|
258
|
+
base_dir: base_dir,
|
|
259
|
+
retries: retries,
|
|
260
|
+
retry_delay: retry_delay
|
|
242
261
|
)
|
|
243
262
|
results = runner.run(task)
|
|
244
|
-
|
|
245
|
-
|
|
263
|
+
if options[:format] == 'json'
|
|
264
|
+
print_results_json(task, results)
|
|
265
|
+
else
|
|
266
|
+
# Don't show summary here - it will be shown at the end for all tasks
|
|
267
|
+
print_results(results, task, show_summary: false, debug: debug_mode)
|
|
268
|
+
end
|
|
246
269
|
results
|
|
247
270
|
end
|
|
248
271
|
|
|
272
|
+
def any_failed?(all_results)
|
|
273
|
+
all_results.values.any? do |task_results|
|
|
274
|
+
task_results.values.any? { |result| result[:status] == :failed }
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
249
278
|
def print_all_tasks_summary(all_results)
|
|
250
279
|
debug_mode = options[:debug] || false
|
|
251
280
|
formatter = OutputFormatter.new(debug: debug_mode)
|
|
@@ -272,6 +301,76 @@ module Kdeploy
|
|
|
272
301
|
end
|
|
273
302
|
end
|
|
274
303
|
|
|
304
|
+
def print_results_json(task_name, results)
|
|
305
|
+
payload = {
|
|
306
|
+
task: task_name.to_s,
|
|
307
|
+
results: results.transform_values { |r| serialize_host_result(r) }
|
|
308
|
+
}
|
|
309
|
+
puts JSON.generate(payload)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def print_dry_run_json(hosts, task_name)
|
|
313
|
+
tasks = self.class.kdeploy_tasks
|
|
314
|
+
commands = tasks[task_name][:block].call
|
|
315
|
+
|
|
316
|
+
payload = {
|
|
317
|
+
task: task_name.to_s,
|
|
318
|
+
dry_run: true,
|
|
319
|
+
planned: hosts.transform_values do |_config|
|
|
320
|
+
commands.map { |cmd| serialize_planned_step(cmd) }
|
|
321
|
+
end
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
puts JSON.generate(payload)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def serialize_host_result(result)
|
|
328
|
+
{
|
|
329
|
+
status: result[:status].to_s,
|
|
330
|
+
error: result[:error],
|
|
331
|
+
steps: Array(result[:output]).map { |step| serialize_step(step) }
|
|
332
|
+
}
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def serialize_step(step)
|
|
336
|
+
out = {
|
|
337
|
+
type: step[:type].to_s,
|
|
338
|
+
command: step[:command],
|
|
339
|
+
duration: step[:duration]
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
out[:result] = step[:result] if step[:type] == :sync
|
|
343
|
+
|
|
344
|
+
if options[:debug] && step[:type] == :run && step[:output].is_a?(Hash)
|
|
345
|
+
out[:stdout] = step[:output][:stdout]
|
|
346
|
+
out[:stderr] = step[:output][:stderr]
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
out
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def serialize_planned_step(cmd)
|
|
353
|
+
case cmd[:type]
|
|
354
|
+
when :run
|
|
355
|
+
{ type: 'run', command: cmd[:command], sudo: cmd[:sudo] }
|
|
356
|
+
when :upload
|
|
357
|
+
{ type: 'upload', source: cmd[:source], destination: cmd[:destination] }
|
|
358
|
+
when :upload_template
|
|
359
|
+
{ type: 'upload_template', source: cmd[:source], destination: cmd[:destination], variables: cmd[:variables] }
|
|
360
|
+
when :sync
|
|
361
|
+
{
|
|
362
|
+
type: 'sync',
|
|
363
|
+
source: cmd[:source],
|
|
364
|
+
destination: cmd[:destination],
|
|
365
|
+
ignore: cmd[:ignore] || [],
|
|
366
|
+
exclude: cmd[:exclude] || [],
|
|
367
|
+
delete: cmd[:delete] || false
|
|
368
|
+
}
|
|
369
|
+
else
|
|
370
|
+
{ type: cmd[:type].to_s }
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
275
374
|
def extract_error_message(result)
|
|
276
375
|
return result[:error] if result[:error]
|
|
277
376
|
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
module Kdeploy
|
|
4
4
|
# Executes a single command and records execution time
|
|
5
5
|
class CommandExecutor
|
|
6
|
-
def initialize(executor, output, debug: false)
|
|
6
|
+
def initialize(executor, output, debug: false, retries: 0, retry_delay: 1)
|
|
7
7
|
@executor = executor
|
|
8
8
|
@output = output
|
|
9
9
|
@debug = debug
|
|
10
|
+
@retries = retries.to_i
|
|
11
|
+
@retry_delay = retry_delay.to_f
|
|
10
12
|
end
|
|
11
13
|
|
|
12
14
|
def execute_run(command, host_name)
|
|
@@ -18,21 +20,19 @@ module Kdeploy
|
|
|
18
20
|
pastel = @output.respond_to?(:pastel) ? @output.pastel : Pastel.new
|
|
19
21
|
|
|
20
22
|
result, duration = measure_time do
|
|
21
|
-
@executor.execute(cmd, use_sudo: use_sudo)
|
|
23
|
+
with_retries { @executor.execute(cmd, use_sudo: use_sudo) }
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
# Show execution time if command took more than 1 second
|
|
25
27
|
@output.write_line(pastel.dim(" [completed in #{format('%.2f', duration)}s]")) if duration > 1.0
|
|
26
28
|
|
|
27
|
-
# Show command output only in debug mode
|
|
28
|
-
show_command_output(result) if @debug
|
|
29
29
|
{ command: cmd, output: result, duration: duration, type: :run }
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def execute_upload(command, host_name)
|
|
33
33
|
show_command_header(host_name, :upload, "#{command[:source]} -> #{command[:destination]}")
|
|
34
34
|
_result, duration = measure_time do
|
|
35
|
-
@executor.upload(command[:source], command[:destination])
|
|
35
|
+
with_retries { @executor.upload(command[:source], command[:destination]) }
|
|
36
36
|
end
|
|
37
37
|
{
|
|
38
38
|
command: "upload: #{command[:source]} -> #{command[:destination]}",
|
|
@@ -44,7 +44,9 @@ module Kdeploy
|
|
|
44
44
|
def execute_upload_template(command, host_name)
|
|
45
45
|
show_command_header(host_name, :upload_template, "#{command[:source]} -> #{command[:destination]}")
|
|
46
46
|
_result, duration = measure_time do
|
|
47
|
-
|
|
47
|
+
with_retries do
|
|
48
|
+
@executor.upload_template(command[:source], command[:destination], command[:variables])
|
|
49
|
+
end
|
|
48
50
|
end
|
|
49
51
|
{
|
|
50
52
|
command: "upload_template: #{command[:source]} -> #{command[:destination]}",
|
|
@@ -60,13 +62,15 @@ module Kdeploy
|
|
|
60
62
|
show_command_header(host_name, :sync, description)
|
|
61
63
|
|
|
62
64
|
result, duration = measure_time do
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
with_retries do
|
|
66
|
+
@executor.sync_directory(
|
|
67
|
+
source,
|
|
68
|
+
destination,
|
|
69
|
+
ignore: command[:ignore] || [],
|
|
70
|
+
exclude: command[:exclude] || [],
|
|
71
|
+
delete: command[:delete] || false
|
|
72
|
+
)
|
|
73
|
+
end
|
|
70
74
|
end
|
|
71
75
|
|
|
72
76
|
build_sync_result(source, destination, result, duration)
|
|
@@ -99,27 +103,16 @@ module Kdeploy
|
|
|
99
103
|
[result, duration]
|
|
100
104
|
end
|
|
101
105
|
|
|
102
|
-
def
|
|
103
|
-
|
|
106
|
+
def with_retries
|
|
107
|
+
attempts = 0
|
|
108
|
+
begin
|
|
109
|
+
attempts += 1
|
|
110
|
+
yield
|
|
111
|
+
rescue SSHError, SCPError, TemplateError
|
|
112
|
+
raise if attempts > (@retries + 1)
|
|
104
113
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
show_stderr(output[:stderr], pastel)
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def show_stdout(stdout)
|
|
111
|
-
return unless stdout && !stdout.empty?
|
|
112
|
-
|
|
113
|
-
stdout.each_line do |line|
|
|
114
|
-
@output.write_line(" #{line.rstrip}") unless line.strip.empty?
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def show_stderr(stderr, pastel)
|
|
119
|
-
return unless stderr && !stderr.empty?
|
|
120
|
-
|
|
121
|
-
stderr.each_line do |line|
|
|
122
|
-
@output.write_line(pastel.green(" #{line.rstrip}")) unless line.strip.empty?
|
|
114
|
+
sleep(@retry_delay) if @retry_delay.positive?
|
|
115
|
+
retry
|
|
123
116
|
end
|
|
124
117
|
end
|
|
125
118
|
|
|
@@ -8,17 +8,23 @@ module Kdeploy
|
|
|
8
8
|
DEFAULT_PARALLEL = 10
|
|
9
9
|
DEFAULT_SSH_TIMEOUT = 30
|
|
10
10
|
DEFAULT_VERIFY_HOST_KEY = :never
|
|
11
|
+
DEFAULT_RETRIES = 0
|
|
12
|
+
DEFAULT_RETRY_DELAY = 1
|
|
11
13
|
CONFIG_FILE_NAME = '.kdeploy.yml'
|
|
12
14
|
|
|
13
15
|
class << self
|
|
14
16
|
attr_accessor :default_parallel,
|
|
15
17
|
:default_ssh_timeout,
|
|
16
|
-
:default_verify_host_key
|
|
18
|
+
:default_verify_host_key,
|
|
19
|
+
:default_retries,
|
|
20
|
+
:default_retry_delay
|
|
17
21
|
|
|
18
22
|
def reset
|
|
19
23
|
@default_parallel = DEFAULT_PARALLEL
|
|
20
24
|
@default_ssh_timeout = DEFAULT_SSH_TIMEOUT
|
|
21
25
|
@default_verify_host_key = DEFAULT_VERIFY_HOST_KEY
|
|
26
|
+
@default_retries = DEFAULT_RETRIES
|
|
27
|
+
@default_retry_delay = DEFAULT_RETRY_DELAY
|
|
22
28
|
end
|
|
23
29
|
|
|
24
30
|
def load_from_file(config_path = nil)
|
|
@@ -56,6 +62,8 @@ module Kdeploy
|
|
|
56
62
|
@default_parallel = config['parallel'] if config.key?('parallel')
|
|
57
63
|
@default_ssh_timeout = config['ssh_timeout'] if config.key?('ssh_timeout')
|
|
58
64
|
@default_verify_host_key = parse_verify_host_key(config['verify_host_key']) if config.key?('verify_host_key')
|
|
65
|
+
@default_retries = config['retries'] if config.key?('retries')
|
|
66
|
+
@default_retry_delay = config['retry_delay'] if config.key?('retry_delay')
|
|
59
67
|
end
|
|
60
68
|
|
|
61
69
|
def parse_verify_host_key(value)
|
data/lib/kdeploy/dsl.rb
CHANGED
|
@@ -5,12 +5,33 @@ require 'set'
|
|
|
5
5
|
module Kdeploy
|
|
6
6
|
# Domain-specific language for defining hosts, roles, and tasks
|
|
7
7
|
module DSL
|
|
8
|
+
def self.included(base)
|
|
9
|
+
# Support `include Kdeploy::DSL` by promoting the DSL methods to class methods.
|
|
10
|
+
# This keeps tests and external integrations simpler while preserving the
|
|
11
|
+
# primary usage pattern (CLI uses `extend DSL`).
|
|
12
|
+
base.extend(self)
|
|
13
|
+
end
|
|
14
|
+
|
|
8
15
|
def self.extended(base)
|
|
9
16
|
base.instance_variable_set(:@kdeploy_hosts, {})
|
|
10
17
|
base.instance_variable_set(:@kdeploy_tasks, {})
|
|
11
18
|
base.instance_variable_set(:@kdeploy_roles, {})
|
|
12
19
|
end
|
|
13
20
|
|
|
21
|
+
# Stable read accessors for tests/integrations.
|
|
22
|
+
# Keep these as aliases so internal storage can evolve without breaking callers.
|
|
23
|
+
def hosts
|
|
24
|
+
kdeploy_hosts
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def tasks
|
|
28
|
+
kdeploy_tasks
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def roles
|
|
32
|
+
kdeploy_roles
|
|
33
|
+
end
|
|
34
|
+
|
|
14
35
|
def kdeploy_hosts
|
|
15
36
|
@kdeploy_hosts ||= {}
|
|
16
37
|
end
|
data/lib/kdeploy/file_filter.rb
CHANGED
|
@@ -47,8 +47,14 @@ module Kdeploy
|
|
|
47
47
|
.gsub('[!', '[^') # [^...] negation
|
|
48
48
|
.gsub('[', '[') # Character class
|
|
49
49
|
|
|
50
|
-
#
|
|
51
|
-
|
|
50
|
+
# gitignore semantics: patterns without a slash match in any directory.
|
|
51
|
+
# e.g. "*.log" should match "a.log" and "logs/a.log".
|
|
52
|
+
if !pattern.include?('/') && !pattern.start_with?('**')
|
|
53
|
+
regex_str = "(?:^|.*/)#{regex_str}"
|
|
54
|
+
elsif !pattern.start_with?('**')
|
|
55
|
+
regex_str = "^#{regex_str}"
|
|
56
|
+
end
|
|
57
|
+
|
|
52
58
|
# Match end of string or directory separator
|
|
53
59
|
regex_str = "#{regex_str}(/|$)" unless pattern.end_with?('*') || pattern.end_with?('**')
|
|
54
60
|
|
|
@@ -64,8 +70,13 @@ module Kdeploy
|
|
|
64
70
|
def relative_path_for(file_path, base_path)
|
|
65
71
|
return file_path.to_s unless base_path
|
|
66
72
|
|
|
73
|
+
file = Pathname.new(file_path.to_s)
|
|
74
|
+
|
|
75
|
+
# If caller already passed a relative path (common in sync traversal),
|
|
76
|
+
# keep it as-is to avoid Pathname#relative_path_from prefix errors.
|
|
77
|
+
return file_path.to_s unless file.absolute?
|
|
78
|
+
|
|
67
79
|
base = Pathname.new(base_path)
|
|
68
|
-
file = Pathname.new(file_path)
|
|
69
80
|
file.relative_path_from(base).to_s
|
|
70
81
|
end
|
|
71
82
|
end
|
|
@@ -27,8 +27,13 @@ module Kdeploy
|
|
|
27
27
|
<<~COMMANDS
|
|
28
28
|
#{@pastel.bright_yellow('🚀')} #{@pastel.bright_white('execute TASK_FILE [TASK]')} Execute deployment tasks from file
|
|
29
29
|
#{@pastel.dim(' --limit HOSTS')} Limit to specific hosts (comma-separated)
|
|
30
|
-
#{@pastel.dim(' --parallel NUM')} Number of parallel executions (default:
|
|
30
|
+
#{@pastel.dim(' --parallel NUM')} Number of parallel executions (default: 10; overridden by .kdeploy.yml)
|
|
31
31
|
#{@pastel.dim(' --dry-run')} Show what would be done without executing
|
|
32
|
+
#{@pastel.dim(' --debug')} Show detailed command output (stdout/stderr)
|
|
33
|
+
#{@pastel.dim(' --no-banner')} Do not print banner (automation-friendly)
|
|
34
|
+
#{@pastel.dim(' --format FORMAT')} Output format (text|json)
|
|
35
|
+
#{@pastel.dim(' --retries N')} Retry count for network operations (default: 0; overridden by .kdeploy.yml)
|
|
36
|
+
#{@pastel.dim(' --retry-delay SECONDS')} Retry delay seconds (default: 1; overridden by .kdeploy.yml)
|
|
32
37
|
|
|
33
38
|
#{@pastel.bright_yellow('🆕')} #{@pastel.bright_white('init [DIR]')} Initialize new deployment project
|
|
34
39
|
#{@pastel.bright_yellow('ℹ️')} #{@pastel.bright_white('version')} Show version information
|
|
@@ -54,6 +59,12 @@ module Kdeploy
|
|
|
54
59
|
|
|
55
60
|
#{@pastel.dim('# Preview deployment')}
|
|
56
61
|
#{@pastel.bright_cyan('kdeploy execute deploy.rb deploy_web --dry-run')}
|
|
62
|
+
|
|
63
|
+
#{@pastel.dim('# Machine-readable output')}
|
|
64
|
+
#{@pastel.bright_cyan('kdeploy execute deploy.rb deploy_web --format json --no-banner')}
|
|
65
|
+
|
|
66
|
+
#{@pastel.dim('# Retry transient network failures')}
|
|
67
|
+
#{@pastel.bright_cyan('kdeploy execute deploy.rb deploy_web --retries 3 --retry-delay 1')}
|
|
57
68
|
EXAMPLES
|
|
58
69
|
end
|
|
59
70
|
|
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,8 +35,8 @@ 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?
|
|
@@ -97,47 +99,53 @@ module Kdeploy
|
|
|
97
99
|
@results
|
|
98
100
|
end
|
|
99
101
|
|
|
100
|
-
def create_task_futures(task)
|
|
102
|
+
def create_task_futures(task, task_name)
|
|
101
103
|
# Store host names in order to match with futures
|
|
102
104
|
@host_names = @hosts.keys
|
|
103
105
|
@hosts.map do |name, config|
|
|
104
106
|
Concurrent::Future.execute(executor: @pool) do
|
|
105
|
-
execute_task_for_host(name, config, task)
|
|
107
|
+
execute_task_for_host(name, config, task, task_name)
|
|
106
108
|
end
|
|
107
109
|
end
|
|
108
110
|
end
|
|
109
111
|
|
|
110
112
|
private
|
|
111
113
|
|
|
112
|
-
def execute_task_for_host(name, config, task)
|
|
114
|
+
def execute_task_for_host(name, config, task, task_name)
|
|
113
115
|
# Add base_dir to config for path resolution
|
|
114
116
|
config_with_base_dir = config.merge(base_dir: @base_dir)
|
|
115
117
|
executor = Executor.new(config_with_base_dir)
|
|
116
|
-
command_executor = CommandExecutor.new(
|
|
118
|
+
command_executor = CommandExecutor.new(
|
|
119
|
+
executor,
|
|
120
|
+
@output,
|
|
121
|
+
debug: @debug,
|
|
122
|
+
retries: @retries,
|
|
123
|
+
retry_delay: @retry_delay
|
|
124
|
+
)
|
|
117
125
|
result = { status: :success, output: [] }
|
|
118
126
|
|
|
119
127
|
begin
|
|
120
|
-
execute_grouped_commands(task, command_executor, name, result)
|
|
128
|
+
execute_grouped_commands(task, command_executor, name, result, task_name)
|
|
121
129
|
rescue StandardError => e
|
|
122
130
|
# Ensure result is always set, even on error
|
|
123
131
|
# Don't re-raise, as it would cause future.value to fail
|
|
124
|
-
result = { status: :failed, error: e.message, output: [] }
|
|
132
|
+
result = { status: :failed, error: "#{e.class}: #{e.message}", output: [] }
|
|
125
133
|
end
|
|
126
134
|
|
|
127
135
|
# Return the result so it can be collected from the future
|
|
128
136
|
[name, result]
|
|
129
137
|
end
|
|
130
138
|
|
|
131
|
-
def execute_grouped_commands(task, command_executor, name, result)
|
|
139
|
+
def execute_grouped_commands(task, command_executor, name, result, task_name)
|
|
132
140
|
commands = task[:block].call
|
|
133
141
|
grouped_commands = CommandGrouper.group(commands)
|
|
134
142
|
|
|
135
143
|
grouped_commands.each_value do |command_group|
|
|
136
|
-
execute_command_group(command_group, command_executor, name, result)
|
|
144
|
+
execute_command_group(command_group, command_executor, name, result, task_name)
|
|
137
145
|
end
|
|
138
146
|
end
|
|
139
147
|
|
|
140
|
-
def execute_command_group(command_group, command_executor, name, result)
|
|
148
|
+
def execute_command_group(command_group, command_executor, name, result, task_name)
|
|
141
149
|
first_cmd = command_group.first
|
|
142
150
|
task_desc = CommandGrouper.task_description(first_cmd)
|
|
143
151
|
show_task_header(task_desc)
|
|
@@ -149,7 +157,7 @@ module Kdeploy
|
|
|
149
157
|
@output.write_line(pastel.dim(" [Step #{index + 1}/#{command_group.length}]"))
|
|
150
158
|
end
|
|
151
159
|
|
|
152
|
-
step_result =
|
|
160
|
+
step_result = execute_command_with_context(command_executor, command, name, task_name)
|
|
153
161
|
result[:output] << step_result
|
|
154
162
|
end
|
|
155
163
|
end
|
|
@@ -169,6 +177,29 @@ module Kdeploy
|
|
|
169
177
|
end
|
|
170
178
|
end
|
|
171
179
|
|
|
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}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def step_description(command)
|
|
188
|
+
case command[:type]
|
|
189
|
+
when :run
|
|
190
|
+
first = command[:command].to_s.lines.first&.strip
|
|
191
|
+
"run: #{first}"
|
|
192
|
+
when :upload
|
|
193
|
+
"upload: #{command[:source]} -> #{command[:destination]}"
|
|
194
|
+
when :upload_template
|
|
195
|
+
"upload_template: #{command[:source]} -> #{command[:destination]}"
|
|
196
|
+
when :sync
|
|
197
|
+
"sync: #{command[:source]} -> #{command[:destination]}"
|
|
198
|
+
else
|
|
199
|
+
command[:type].to_s
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
172
203
|
def show_task_header(task_desc)
|
|
173
204
|
# Don't show command header during execution - it will be shown in results
|
|
174
205
|
# This reduces noise during execution
|
data/lib/kdeploy/version.rb
CHANGED
data/r.md
ADDED
|
File without changes
|
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.33
|
|
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-01-25 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
|
|
@@ -184,6 +185,7 @@ files:
|
|
|
184
185
|
- lib/kdeploy/runner.rb
|
|
185
186
|
- lib/kdeploy/template.rb
|
|
186
187
|
- lib/kdeploy/version.rb
|
|
188
|
+
- r.md
|
|
187
189
|
homepage: https://github.com/kevin197011/kdeploy
|
|
188
190
|
licenses:
|
|
189
191
|
- MIT
|