kdeploy 1.2.41 → 1.3.1

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: d9bab827b041d7b33e3486ddcddc0956fa7b00fddbd20275709c8e23f5201f7a
4
- data.tar.gz: c7eee9c23f27b9541a4ef09c253ae5bffa9e9ae5715a941eb36f363afb5a6122
3
+ metadata.gz: 782163100dc54c6588d66c1655434a9ab4f6a0cd3aa5d97583e4b9e5fb757bbd
4
+ data.tar.gz: 38c8a9e92920dcdbfdca974a9ed4a51579cb618cc2a80fedcdaf19f0fe73fd80
5
5
  SHA512:
6
- metadata.gz: 9fb85d1b6243a36259f67a6d9c96e1028190a7d650a5b625d4b31f76f5bd2eb12adcce06e07c7c12177ffaa128fa32a561e8b8092674ca51df2d4c6e6d5d2790
7
- data.tar.gz: 6dd1ad95b47ecf2b0d8d09ee3f2994c4caccfb248894f9ca0c374a66fe9121a427726421ad9ceb0ce30512adb34080a7c39bb7c7789022dcff5531ec8131e003
6
+ metadata.gz: bcf659df1c65bfc50c0ebdb57a7b9f689a74c9db7107f7d3c48beb869b37633b2b887f02ca27f8f0b1707c5a593b8727e0416754221654c31faeaa1c1ddc2a3c
7
+ data.tar.gz: 2fa594ddb0cf3196dc1abcfd9ae9fb54729b91907445912a0e10cd4aa5d7da83c42ac9dbab02ee33e8d824b38ef25408f35e7791108e9b26728a1b96507bd816
data/README.md CHANGED
@@ -207,6 +207,8 @@ kdeploy execute deploy.rb deploy_web
207
207
  - `--format FORMAT`: 输出格式(`text`|`json`,默认 `text`)
208
208
  - `--retries N`: 网络相关操作重试次数(默认 `0`)
209
209
  - `--retry-delay SECONDS`: 每次重试间隔秒数(默认 `1`)
210
+ - `--retry-on-nonzero`: 非零退出码重试开关(默认 `false`)
211
+ - `--timeout SECONDS`: 单 host 执行超时(秒,默认不启用)
210
212
 
211
213
  **示例:**
212
214
  ```bash
@@ -228,6 +230,12 @@ kdeploy execute deploy.rb deploy_web --format json --no-banner
228
230
  # 重试网络抖动导致的失败
229
231
  kdeploy execute deploy.rb deploy_web --retries 3 --retry-delay 1
230
232
 
233
+ # 对非零退出码进行重试
234
+ kdeploy execute deploy.rb deploy_web --retries 2 --retry-on-nonzero
235
+
236
+ # 设置单 host 超时(秒)
237
+ kdeploy execute deploy.rb deploy_web --timeout 120
238
+
231
239
  # 组合选项
232
240
  kdeploy execute deploy.rb deploy_web --limit web01 --parallel 3 --dry-run
233
241
  ```
@@ -1141,4 +1149,3 @@ end
1141
1149
  ---
1142
1150
 
1143
1151
  **为 DevOps 社区用 ❤️ 制作**
1144
-
data/README_EN.md CHANGED
@@ -206,6 +206,8 @@ kdeploy execute deploy.rb deploy_web
206
206
  - `--format FORMAT`: Output format (`text`|`json`, default `text`)
207
207
  - `--retries N`: Retry count for network operations (default `0`)
208
208
  - `--retry-delay SECONDS`: Delay between retries in seconds (default `1`)
209
+ - `--retry-on-nonzero`: Retry commands on nonzero exit status (default `false`)
210
+ - `--timeout SECONDS`: Per-host execution timeout in seconds (default: none)
209
211
 
210
212
  **Examples:**
211
213
  ```bash
@@ -227,6 +229,12 @@ kdeploy execute deploy.rb deploy_web --format json --no-banner
227
229
  # Retry transient network failures
228
230
  kdeploy execute deploy.rb deploy_web --retries 3 --retry-delay 1
229
231
 
232
+ # Retry on nonzero exit status
233
+ kdeploy execute deploy.rb deploy_web --retries 2 --retry-on-nonzero
234
+
235
+ # Set per-host timeout (seconds)
236
+ kdeploy execute deploy.rb deploy_web --timeout 120
237
+
230
238
  # Combine options
231
239
  kdeploy execute deploy.rb deploy_web --limit web01 --parallel 3 --dry-run
232
240
  ```
data/lib/kdeploy/cli.rb CHANGED
@@ -51,6 +51,8 @@ module Kdeploy
51
51
  method_option :format, type: :string, default: 'text', desc: 'Output format (text|json)'
52
52
  method_option :retries, type: :numeric, desc: 'Retry count for network operations (default: 0)'
53
53
  method_option :retry_delay, type: :numeric, desc: 'Retry delay seconds (default: 1)'
54
+ method_option :retry_on_nonzero, type: :boolean, desc: 'Retry commands on nonzero exit status (default: false)'
55
+ method_option :timeout, type: :numeric, desc: 'Per-host execution timeout seconds (default: none)'
54
56
  def execute(task_file, task_name = nil)
55
57
  load_config_file
56
58
  show_banner_once
@@ -267,6 +269,9 @@ module Kdeploy
267
269
  debug_mode = options[:debug] || false
268
270
  retries = options[:retries].nil? ? Configuration.default_retries : options[:retries]
269
271
  retry_delay = options[:retry_delay].nil? ? Configuration.default_retry_delay : options[:retry_delay]
272
+ retry_on_nonzero =
273
+ options[:retry_on_nonzero].nil? ? Configuration.default_retry_on_nonzero : options[:retry_on_nonzero]
274
+ host_timeout = options[:timeout].nil? ? Configuration.default_host_timeout : options[:timeout]
270
275
  base_dir = @task_file_dir
271
276
  runner = Runner.new(
272
277
  hosts, self.class.kdeploy_tasks,
@@ -275,7 +280,9 @@ module Kdeploy
275
280
  debug: debug_mode,
276
281
  base_dir: base_dir,
277
282
  retries: retries,
278
- retry_delay: retry_delay
283
+ retry_delay: retry_delay,
284
+ retry_on_nonzero: retry_on_nonzero,
285
+ host_timeout: host_timeout
279
286
  )
280
287
  results = runner.run(task)
281
288
  if options[:format] == 'json'
@@ -363,6 +370,8 @@ module Kdeploy
363
370
  duration: step[:duration]
364
371
  }
365
372
 
373
+ out[:exit_status] = step[:output][:exit_status] if step[:output].is_a?(Hash) && step[:output].key?(:exit_status)
374
+
366
375
  out[:result] = step[:result] if step[:type] == :sync
367
376
 
368
377
  if options[:debug] && step[:type] == :run && step[:output].is_a?(Hash)
@@ -3,12 +3,13 @@
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, retries: 0, retry_delay: 1)
6
+ def initialize(executor, output, debug: false, retries: 0, retry_delay: 1, retry_on_nonzero: false)
7
7
  @executor = executor
8
8
  @output = output
9
9
  @debug = debug
10
10
  @retries = retries.to_i
11
11
  @retry_delay = retry_delay.to_f
12
+ @retry_on_nonzero = retry_on_nonzero
12
13
  end
13
14
 
14
15
  def execute_run(command, _host_name)
@@ -91,7 +92,8 @@ module Kdeploy
91
92
  begin
92
93
  attempts += 1
93
94
  yield
94
- rescue SSHError, SCPError, TemplateError
95
+ rescue SSHError, SCPError, TemplateError => e
96
+ raise if e.is_a?(SSHError) && e.exit_status && !@retry_on_nonzero
95
97
  raise if attempts > (@retries + 1)
96
98
 
97
99
  sleep(@retry_delay) if @retry_delay.positive?
@@ -10,6 +10,8 @@ module Kdeploy
10
10
  DEFAULT_VERIFY_HOST_KEY = :never
11
11
  DEFAULT_RETRIES = 0
12
12
  DEFAULT_RETRY_DELAY = 1
13
+ DEFAULT_HOST_TIMEOUT = nil
14
+ DEFAULT_RETRY_ON_NONZERO = false
13
15
  CONFIG_FILE_NAME = '.kdeploy.yml'
14
16
 
15
17
  class << self
@@ -17,7 +19,9 @@ module Kdeploy
17
19
  :default_ssh_timeout,
18
20
  :default_verify_host_key,
19
21
  :default_retries,
20
- :default_retry_delay
22
+ :default_retry_delay,
23
+ :default_host_timeout,
24
+ :default_retry_on_nonzero
21
25
 
22
26
  def reset
23
27
  @default_parallel = DEFAULT_PARALLEL
@@ -25,6 +29,8 @@ module Kdeploy
25
29
  @default_verify_host_key = DEFAULT_VERIFY_HOST_KEY
26
30
  @default_retries = DEFAULT_RETRIES
27
31
  @default_retry_delay = DEFAULT_RETRY_DELAY
32
+ @default_host_timeout = DEFAULT_HOST_TIMEOUT
33
+ @default_retry_on_nonzero = DEFAULT_RETRY_ON_NONZERO
28
34
  end
29
35
 
30
36
  def load_from_file(config_path = nil)
@@ -64,6 +70,8 @@ module Kdeploy
64
70
  @default_verify_host_key = parse_verify_host_key(config['verify_host_key']) if config.key?('verify_host_key')
65
71
  @default_retries = config['retries'] if config.key?('retries')
66
72
  @default_retry_delay = config['retry_delay'] if config.key?('retry_delay')
73
+ @default_host_timeout = config['host_timeout'] if config.key?('host_timeout')
74
+ @default_retry_on_nonzero = config['retry_on_nonzero'] if config.key?('retry_on_nonzero')
67
75
  end
68
76
 
69
77
  def parse_verify_host_key(value)
@@ -34,6 +34,8 @@ module Kdeploy
34
34
  #{@pastel.dim(' --format FORMAT')} Output format (text|json)
35
35
  #{@pastel.dim(' --retries N')} Retry count for network operations (default: 0; overridden by .kdeploy.yml)
36
36
  #{@pastel.dim(' --retry-delay SECONDS')} Retry delay seconds (default: 1; overridden by .kdeploy.yml)
37
+ #{@pastel.dim(' --retry-on-nonzero')} Retry commands on nonzero exit status (default: false; overridden by .kdeploy.yml)
38
+ #{@pastel.dim(' --timeout SECONDS')} Per-host execution timeout seconds (default: none; overridden by .kdeploy.yml)
37
39
 
38
40
  #{@pastel.bright_yellow('🆕')} #{@pastel.bright_white('init [DIR]')} Initialize new deployment project
39
41
  #{@pastel.bright_yellow('ℹ️')} #{@pastel.bright_white('version')} Show version information
@@ -7,7 +7,9 @@ module Kdeploy
7
7
  class Runner
8
8
  def initialize(hosts, tasks, parallel: Configuration.default_parallel, output: ConsoleOutput.new,
9
9
  debug: false, base_dir: nil, retries: Configuration.default_retries,
10
- retry_delay: Configuration.default_retry_delay)
10
+ retry_delay: Configuration.default_retry_delay,
11
+ retry_on_nonzero: Configuration.default_retry_on_nonzero,
12
+ host_timeout: Configuration.default_host_timeout)
11
13
  @hosts = hosts
12
14
  @tasks = tasks
13
15
  @parallel = parallel
@@ -16,6 +18,8 @@ module Kdeploy
16
18
  @base_dir = base_dir
17
19
  @retries = retries
18
20
  @retry_delay = retry_delay
21
+ @retry_on_nonzero = retry_on_nonzero
22
+ @host_timeout = normalize_timeout(host_timeout)
19
23
  @pool = Concurrent::FixedThreadPool.new(@parallel)
20
24
  @results = Concurrent::Hash.new
21
25
  end
@@ -41,59 +45,48 @@ module Kdeploy
41
45
  # If no hosts, return empty results immediately
42
46
  return @results if futures.empty?
43
47
 
44
- # Collect results from futures
45
- futures.each_with_index do |future, index|
46
- host_name = @host_names[index] # Get host name from the stored list
47
- begin
48
- # Wait for future to complete and get its value
49
- # This ensures the future has finished executing
50
- future_result = future.value
51
-
52
- # Handle the result
53
- if future_result.nil?
54
- # Future returned nil - create a default result
55
- @results[host_name] = { status: :unknown, error: 'Future returned nil', output: [] }
56
- elsif future_result.is_a?(Array) && future_result.length == 2
57
- name, result = future_result
58
- # Store the result using the name from the future
59
- @results[name] = result
60
- else
61
- # Handle unexpected result format - create a default result
62
- @results[host_name] = {
63
- status: :unknown,
64
- error: "Unexpected result format: #{future_result.class}",
48
+ pending = futures.dup
49
+
50
+ until pending.empty?
51
+ progressed = false
52
+ now = Time.now
53
+
54
+ pending.dup.each do |future|
55
+ meta = @future_meta[future]
56
+ host_name = meta[:host_name]
57
+ started_at = meta[:started_at].get
58
+
59
+ if future.fulfilled? || future.rejected?
60
+ collect_future_result(future, host_name)
61
+ pending.delete(future)
62
+ progressed = true
63
+ elsif timeout_exceeded?(started_at, now)
64
+ @results[host_name] ||= {
65
+ status: :failed,
66
+ error: "execution timeout after #{@host_timeout}s",
65
67
  output: []
66
68
  }
69
+ pending.delete(future)
70
+ progressed = true
67
71
  end
68
-
69
- # Check if future raised an exception
70
- if future.rejected?
71
- error = begin
72
- future.reason
73
- rescue StandardError
74
- 'Unknown error'
75
- end
76
- @results[host_name] = { status: :failed, error: error, output: [] } unless @results.key?(host_name)
77
- end
78
- rescue StandardError => e
79
- # If future.value raises an exception, create an error result
80
- @results[host_name] = { status: :failed, error: "#{e.class}: #{e.message}", output: [] }
81
- ensure
82
- # Ensure we always have a result for this host
83
- @results[host_name] ||= { status: :unknown, error: 'No result collected', output: [] }
84
72
  end
73
+
74
+ sleep(0.05) unless progressed
85
75
  end
86
76
 
87
77
  @results
88
78
  end
89
79
 
90
80
  def create_task_futures(task, task_name)
91
- # Store host names in order to match with futures
92
- @host_names = @hosts.keys
81
+ @future_meta = {}
93
82
  @hosts.map do |name, config|
94
- Concurrent::Future.execute(executor: @pool) do
83
+ started_at = Concurrent::AtomicReference.new(nil)
84
+ future = Concurrent::Future.execute(executor: @pool) do
85
+ started_at.set(Time.now)
95
86
  execute_task_for_host(name, config, task, task_name)
96
87
  end
88
+ @future_meta[future] = { host_name: name, started_at: started_at }
89
+ future
97
90
  end
98
91
  end
99
92
 
@@ -108,7 +101,8 @@ module Kdeploy
108
101
  @output,
109
102
  debug: @debug,
110
103
  retries: @retries,
111
- retry_delay: @retry_delay
104
+ retry_delay: @retry_delay,
105
+ retry_on_nonzero: @retry_on_nonzero
112
106
  )
113
107
  result = { status: :success, output: [] }
114
108
 
@@ -133,7 +127,7 @@ module Kdeploy
133
127
  rescue StandardError => e
134
128
  step = step_description(command)
135
129
  result[:status] = :failed
136
- result[:error] = "task=#{task_name} host=#{name} step=#{step} error=#{e.class}: #{e.message}"
130
+ result[:error] = build_step_error(task_name, name, step, e)
137
131
  result[:output] << {
138
132
  type: command[:type],
139
133
  command: step_command_string(command),
@@ -145,6 +139,53 @@ module Kdeploy
145
139
  end
146
140
  end
147
141
 
142
+ def collect_future_result(future, host_name)
143
+ return if @results.key?(host_name)
144
+
145
+ begin
146
+ future_result = future.value
147
+ if future_result.nil?
148
+ @results[host_name] = { status: :unknown, error: 'Future returned nil', output: [] }
149
+ elsif future_result.is_a?(Array) && future_result.length == 2
150
+ name, result = future_result
151
+ @results[name] = result
152
+ else
153
+ @results[host_name] = {
154
+ status: :unknown,
155
+ error: "Unexpected result format: #{future_result.class}",
156
+ output: []
157
+ }
158
+ end
159
+
160
+ if future.rejected?
161
+ error = begin
162
+ future.reason
163
+ rescue StandardError
164
+ 'Unknown error'
165
+ end
166
+ @results[host_name] ||= { status: :failed, error: error, output: [] }
167
+ end
168
+ rescue StandardError => e
169
+ @results[host_name] = { status: :failed, error: "#{e.class}: #{e.message}", output: [] }
170
+ ensure
171
+ @results[host_name] ||= { status: :unknown, error: 'No result collected', output: [] }
172
+ end
173
+ end
174
+
175
+ def timeout_exceeded?(started_at, now)
176
+ return false unless @host_timeout
177
+ return false if started_at.nil?
178
+
179
+ (now - started_at) > @host_timeout
180
+ end
181
+
182
+ def normalize_timeout(timeout)
183
+ return nil if timeout.nil?
184
+
185
+ timeout = timeout.to_f
186
+ timeout.positive? ? timeout : nil
187
+ end
188
+
148
189
  def error_output_for_step(error)
149
190
  return nil unless error.is_a?(Kdeploy::SSHError)
150
191
 
@@ -156,6 +197,17 @@ module Kdeploy
156
197
  }
157
198
  end
158
199
 
200
+ def build_step_error(task_name, host_name, step, error)
201
+ base = "task=#{task_name} host=#{host_name} step=#{step} error=#{error.class}: #{error.message}"
202
+ return base unless error.is_a?(Kdeploy::SSHError)
203
+
204
+ if error.exit_status
205
+ "#{base} exit_status=#{error.exit_status} command=#{error.command}"
206
+ else
207
+ base
208
+ end
209
+ end
210
+
159
211
  def execute_command(command_executor, command, host_name)
160
212
  case command[:type]
161
213
  when :run
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Kdeploy module for version management
4
4
  module Kdeploy
5
- VERSION = '1.2.41' unless const_defined?(:VERSION)
5
+ VERSION = '1.3.1' unless const_defined?(:VERSION)
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kdeploy
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.41
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kk
@@ -184,7 +184,6 @@ files:
184
184
  - lib/kdeploy/runner.rb
185
185
  - lib/kdeploy/template.rb
186
186
  - lib/kdeploy/version.rb
187
- - r.md
188
187
  homepage: https://github.com/kevin197011/kdeploy
189
188
  licenses:
190
189
  - MIT
data/r.md DELETED
@@ -1 +0,0 @@
1
- # Your product documentation