kdeploy 1.3.1 → 1.3.4

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.
@@ -5,6 +5,8 @@ require 'net/scp'
5
5
  require 'pathname'
6
6
  require 'find'
7
7
  require 'shellwords'
8
+ require 'tempfile'
9
+ require 'concurrent'
8
10
  require_relative 'file_filter'
9
11
 
10
12
  module Kdeploy
@@ -16,7 +18,7 @@ module Kdeploy
16
18
  @ip = host_config[:ip]
17
19
  @password = host_config[:password]
18
20
  @key = host_config[:key]
19
- @port = host_config[:port] # 新增端口支持
21
+ @port = host_config[:port] # Added custom port support
20
22
  @use_sudo = host_config[:use_sudo] || false
21
23
  @sudo_password = host_config[:sudo_password]
22
24
  @base_dir = host_config[:base_dir] # Base directory for resolving relative paths
@@ -114,7 +116,8 @@ module Kdeploy
114
116
  raise TemplateError.new("Template upload failed: #{e.message}", e)
115
117
  end
116
118
 
117
- def sync_directory(source, destination, ignore: [], exclude: [], delete: false, use_sudo: nil)
119
+ def sync_directory(source, destination, ignore: [], exclude: [], delete: false, fast: nil, parallel: nil,
120
+ use_sudo: nil)
118
121
  use_sudo = @use_sudo if use_sudo.nil?
119
122
 
120
123
  # Resolve relative paths relative to base_dir
@@ -127,24 +130,29 @@ module Kdeploy
127
130
  all_patterns = ignore + exclude
128
131
  filter = FileFilter.new(ignore_patterns: all_patterns)
129
132
 
133
+ if fast
134
+ rsync_result = sync_with_rsync(
135
+ resolved_source,
136
+ destination,
137
+ ignore: ignore,
138
+ exclude: exclude,
139
+ delete: delete,
140
+ use_sudo: use_sudo
141
+ )
142
+ return rsync_result if rsync_result
143
+ end
144
+
130
145
  # Collect files to sync
131
146
  files_to_sync = collect_files_to_sync(resolved_source, filter)
132
147
 
133
148
  # Upload files
134
- uploaded_count = 0
135
- source_path = Pathname.new(resolved_source)
136
- files_to_sync.each do |file_path|
137
- relative_path = Pathname.new(file_path).relative_path_from(source_path).to_s
138
- remote_path = File.join(destination, relative_path).gsub(%r{/+}, '/')
139
-
140
- # Ensure remote directory exists
141
- remote_dir = File.dirname(remote_path)
142
- ensure_remote_directory(remote_dir, use_sudo: use_sudo)
143
-
144
- # Upload file
145
- upload(file_path, remote_path, use_sudo: use_sudo)
146
- uploaded_count += 1
147
- end
149
+ uploaded_count = upload_files(
150
+ files_to_sync,
151
+ resolved_source,
152
+ destination,
153
+ parallel: parallel,
154
+ use_sudo: use_sudo
155
+ )
148
156
 
149
157
  # Delete extra files if requested
150
158
  deleted_count = 0
@@ -196,6 +204,80 @@ module Kdeploy
196
204
  end
197
205
  end
198
206
 
207
+ def sync_with_rsync(source, destination, ignore:, exclude:, delete:, use_sudo:)
208
+ return nil unless system('command -v rsync >/dev/null 2>&1')
209
+
210
+ exclude_file = build_rsync_excludes(ignore + exclude)
211
+
212
+ unless remote_rsync_available?
213
+ File.delete(exclude_file) if exclude_file && File.exist?(exclude_file)
214
+ return nil
215
+ end
216
+
217
+ begin
218
+ rsync_cmd = build_rsync_command(source, destination, exclude_file, delete: delete, use_sudo: use_sudo)
219
+ return nil unless system(rsync_cmd)
220
+
221
+ {
222
+ uploaded: 0,
223
+ deleted: 0,
224
+ total: 0,
225
+ fast_path: 'rsync'
226
+ }
227
+ ensure
228
+ File.delete(exclude_file) if exclude_file && File.exist?(exclude_file)
229
+ end
230
+ rescue StandardError
231
+ nil
232
+ end
233
+
234
+ def build_rsync_excludes(patterns)
235
+ patterns = Array(patterns).compact
236
+ return nil if patterns.empty?
237
+
238
+ file = Tempfile.new('kdeploy_rsync_excludes')
239
+ patterns.each { |pattern| file.write("#{pattern}\n") }
240
+ file.close
241
+ file.path
242
+ end
243
+
244
+ def build_rsync_command(source, destination, exclude_file, delete:, use_sudo:)
245
+ delete_flag = delete ? '--delete' : ''
246
+ exclude_flag = exclude_file ? "--exclude-from='#{exclude_file}'" : ''
247
+ ssh_cmd = build_rsync_ssh_command
248
+ sudo_flag = if use_sudo || requires_sudo?(destination)
249
+ '--rsync-path="sudo rsync"'
250
+ else
251
+ ''
252
+ end
253
+ parts = [
254
+ 'rsync -az',
255
+ delete_flag,
256
+ exclude_flag,
257
+ sudo_flag,
258
+ "-e \"#{ssh_cmd}\"",
259
+ "#{Shellwords.escape(source)}/",
260
+ "#{@user}@#{@ip}:#{Shellwords.escape(destination)}/"
261
+ ].reject(&:empty?)
262
+ parts.join(' ')
263
+ end
264
+
265
+ def build_rsync_ssh_command
266
+ ssh = ['ssh']
267
+ ssh << "-p #{@port}" if @port
268
+ ssh << "-i #{Shellwords.escape(@key)}" if @key
269
+ ssh.join(' ')
270
+ end
271
+
272
+ def remote_rsync_available?
273
+ Net::SSH.start(@ip, @user, ssh_options) do |ssh|
274
+ output = ssh.exec!('command -v rsync 2>/dev/null || true')
275
+ return !output.to_s.strip.empty?
276
+ end
277
+ rescue StandardError
278
+ false
279
+ end
280
+
199
281
  def requires_sudo?(path)
200
282
  # Check if path is in system directories that typically require sudo
201
283
  system_dirs = %w[/etc /usr /var /opt /sbin /bin /lib /lib64 /root]
@@ -229,16 +311,16 @@ module Kdeploy
229
311
  end
230
312
 
231
313
  def wrap_with_sudo(command)
232
- # 如果命令已经以 sudo 开头,不重复添加
314
+ # Do not add sudo again if command already starts with sudo
233
315
  return command if command.strip.start_with?('sudo')
234
316
 
235
- # 多行、shell 控制结构、或包含 && / || / ; 的复合命令需整段在 sudo 下执行
317
+ # Wrap multi-line/control/compound commands so sudo applies to the full block
236
318
  needs_wrap = command.include?("\n") ||
237
319
  command.match?(/\b(if|for|while|case|function)\b/) ||
238
320
  command.match?(/\s(&&|\|\||;)\s/)
239
321
 
240
322
  if needs_wrap
241
- # 转义命令中的单引号,然后用 bash -c 执行
323
+ # Escape single quotes in command, then execute via bash -c
242
324
  escaped_command = command.gsub("'", "'\"'\"'")
243
325
  if @sudo_password
244
326
  escaped_password = @sudo_password.gsub('\'', "'\"'\"'").gsub('$', '\\$').gsub('`', '\\`')
@@ -247,7 +329,7 @@ module Kdeploy
247
329
  "sudo bash -c '#{escaped_command}'"
248
330
  end
249
331
  elsif @sudo_password
250
- # 单行命令直接包装
332
+ # Wrap single-line command directly
251
333
  escaped_password = @sudo_password.gsub('\'', "'\"'\"'").gsub('$', '\\$').gsub('`', '\\`')
252
334
  "echo '#{escaped_password}' | sudo -S #{command}"
253
335
  else
@@ -271,6 +353,55 @@ module Kdeploy
271
353
  files
272
354
  end
273
355
 
356
+ def upload_files(files_to_sync, source_dir, destination, parallel:, use_sudo:)
357
+ count = Concurrent::AtomicFixnum.new(0)
358
+ source_path = Pathname.new(source_dir)
359
+ parallel = normalize_parallel(parallel)
360
+ return upload_files_sequential(files_to_sync, source_path, destination, use_sudo, count) if parallel <= 1
361
+
362
+ queue = Queue.new
363
+ files_to_sync.each { |path| queue << path }
364
+ workers = Array.new(parallel) do
365
+ Thread.new do
366
+ until queue.empty?
367
+ file_path = begin
368
+ queue.pop(true)
369
+ rescue StandardError
370
+ nil
371
+ end
372
+ next unless file_path
373
+
374
+ upload_single_file(file_path, source_path, destination, use_sudo)
375
+ count.increment
376
+ end
377
+ end
378
+ end
379
+ workers.each(&:join)
380
+ count.value
381
+ end
382
+
383
+ def upload_files_sequential(files_to_sync, source_path, destination, use_sudo, count)
384
+ files_to_sync.each do |file_path|
385
+ upload_single_file(file_path, source_path, destination, use_sudo)
386
+ count.increment
387
+ end
388
+ count.value
389
+ end
390
+
391
+ def upload_single_file(file_path, source_path, destination, use_sudo)
392
+ relative_path = Pathname.new(file_path).relative_path_from(source_path).to_s
393
+ remote_path = File.join(destination, relative_path).gsub(%r{/+}, '/')
394
+ remote_dir = File.dirname(remote_path)
395
+ ensure_remote_directory(remote_dir, use_sudo: use_sudo)
396
+ upload(file_path, remote_path, use_sudo: use_sudo)
397
+ end
398
+
399
+ def normalize_parallel(parallel)
400
+ value = parallel.nil? ? Configuration.default_sync_parallel : parallel
401
+ value = value.to_i
402
+ value.positive? ? value : 1
403
+ end
404
+
274
405
  def ensure_remote_directory(remote_dir, use_sudo: nil)
275
406
  use_sudo = @use_sudo if use_sudo.nil?
276
407
  return if remote_dir.nil? || remote_dir.empty? || remote_dir == '.' || remote_dir == '/'
@@ -18,7 +18,6 @@ module Kdeploy
18
18
  def format_host_status(host, status)
19
19
  status_str = case status
20
20
  when :success then @pastel.green('✓ ok')
21
- when :changed then @pastel.yellow('~ changed')
22
21
  else @pastel.red('✗ failed')
23
22
  end
24
23
  @pastel.bright_white(" #{host.ljust(20)} #{status_str}")
@@ -101,21 +100,18 @@ module Kdeploy
101
100
  end
102
101
 
103
102
  def calculate_summary_counts(result)
104
- ok = %i[success changed].include?(result[:status]) ? result[:output].size : 0
103
+ ok = result[:status] == :success ? result[:output].size : 0
105
104
  failed = result[:status] == :failed ? 1 : 0
106
- changed = result[:status] == :changed ? result[:output].size : 0
107
- { ok: ok, failed: failed, changed: changed }
105
+ { ok: ok, failed: failed }
108
106
  end
109
107
 
110
108
  def build_summary_line(host, counts, max_host_len)
111
109
  ok_w = 7
112
- changed_w = 11
113
110
  failed_w = 10
114
111
 
115
112
  ok_str = @pastel.green("ok=#{counts[:ok].to_s.ljust(ok_w - 3)}")
116
- changed_str = @pastel.yellow("changed=#{counts[:changed].to_s.ljust(changed_w - 8)}")
117
113
  failed_str = @pastel.red("failed=#{counts[:failed].to_s.ljust(failed_w - 7)}")
118
- "#{host.ljust(max_host_len)} : #{ok_str} #{changed_str} #{failed_str}"
114
+ "#{host.ljust(max_host_len)} : #{ok_str} #{failed_str}"
119
115
  end
120
116
 
121
117
  def colorize_summary_line(line, counts)
@@ -261,6 +257,7 @@ module Kdeploy
261
257
  stats = []
262
258
  stats << @pastel.green("#{uploaded} uploaded") if uploaded.positive?
263
259
  stats << @pastel.yellow("#{deleted} deleted") if deleted.positive?
260
+ stats << @pastel.cyan("fast: #{result[:fast_path]}") if result[:fast_path]
264
261
  stats_str = stats.any? ? " (#{stats.join(', ')})" : " (#{total} files)"
265
262
 
266
263
  @pastel.dim(' ') + @pastel.cyan(display_path) + @pastel.dim(stats_str) + duration_str + " #{status_str}"
@@ -9,7 +9,9 @@ module Kdeploy
9
9
  debug: false, base_dir: nil, retries: Configuration.default_retries,
10
10
  retry_delay: Configuration.default_retry_delay,
11
11
  retry_on_nonzero: Configuration.default_retry_on_nonzero,
12
- host_timeout: Configuration.default_host_timeout)
12
+ host_timeout: Configuration.default_host_timeout,
13
+ step_timeout: Configuration.default_step_timeout,
14
+ retry_policy: Configuration.default_retry_policy)
13
15
  @hosts = hosts
14
16
  @tasks = tasks
15
17
  @parallel = parallel
@@ -20,6 +22,8 @@ module Kdeploy
20
22
  @retry_delay = retry_delay
21
23
  @retry_on_nonzero = retry_on_nonzero
22
24
  @host_timeout = normalize_timeout(host_timeout)
25
+ @step_timeout = normalize_timeout(step_timeout)
26
+ @retry_policy = retry_policy
23
27
  @pool = Concurrent::FixedThreadPool.new(@parallel)
24
28
  @results = Concurrent::Hash.new
25
29
  end
@@ -61,6 +65,7 @@ module Kdeploy
61
65
  pending.delete(future)
62
66
  progressed = true
63
67
  elsif timeout_exceeded?(started_at, now)
68
+ future.cancel if future.respond_to?(:cancel)
64
69
  @results[host_name] ||= {
65
70
  status: :failed,
66
71
  error: "execution timeout after #{@host_timeout}s",
@@ -102,7 +107,9 @@ module Kdeploy
102
107
  debug: @debug,
103
108
  retries: @retries,
104
109
  retry_delay: @retry_delay,
105
- retry_on_nonzero: @retry_on_nonzero
110
+ retry_on_nonzero: @retry_on_nonzero,
111
+ step_timeout: @step_timeout,
112
+ retry_policy: @retry_policy
106
113
  )
107
114
  result = { status: :success, output: [] }
108
115
 
@@ -2,12 +2,14 @@
2
2
 
3
3
  require 'erb'
4
4
  require 'tempfile'
5
+ require 'set'
5
6
 
6
7
  module Kdeploy
7
8
  # ERB template rendering and upload handler
8
9
  class Template
9
10
  def self.render(template_path, variables = {})
10
11
  template_content = File.read(template_path)
12
+ validate_template_variables(template_content, variables)
11
13
  context = create_template_context(variables)
12
14
  ERB.new(template_content).result(context.instance_eval { binding })
13
15
  end
@@ -24,17 +26,38 @@ module Kdeploy
24
26
  def self.render_and_upload(executor, template_path, destination, variables = {})
25
27
  rendered_content = render(template_path, variables)
26
28
 
27
- # 创建临时文件
29
+ # Create temporary file
28
30
  temp_file = Tempfile.new('kdeploy')
29
31
  begin
30
32
  temp_file.write(rendered_content)
31
33
  temp_file.close
32
34
 
33
- # 上传渲染后的文件
35
+ # Upload rendered file
34
36
  executor.upload(temp_file.path, destination)
35
37
  ensure
36
38
  temp_file.unlink
37
39
  end
38
40
  end
41
+
42
+ def self.validate_template_variables(template_content, variables)
43
+ required = extract_template_identifiers(template_content)
44
+ return if required.empty?
45
+
46
+ provided = variables.keys.to_set(&:to_s)
47
+ missing = required.reject { |name| provided.include?(name) }
48
+ return if missing.empty?
49
+
50
+ raise ArgumentError, "Missing template variables: #{missing.sort.join(', ')}"
51
+ end
52
+
53
+ def self.extract_template_identifiers(template_content)
54
+ identifiers = template_content.scan(/<%=\s*([a-zA-Z_]\w*)/).flatten
55
+ identifiers.uniq - ruby_keywords
56
+ end
57
+
58
+ def self.ruby_keywords
59
+ %w[alias and begin break case class def defined? do else elsif end ensure false for if in module next nil not
60
+ or redo rescue retry return self super then true undef unless until when while yield]
61
+ end
39
62
  end
40
63
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Kdeploy module for version management
4
4
  module Kdeploy
5
- VERSION = '1.3.1' unless const_defined?(:VERSION)
5
+ VERSION = '1.3.4' unless const_defined?(:VERSION)
6
6
  end
data/lib/kdeploy.rb CHANGED
@@ -2,20 +2,20 @@
2
2
 
3
3
  require_relative 'kdeploy/version'
4
4
  require_relative 'kdeploy/errors'
5
- require_relative 'kdeploy/configuration'
6
- require_relative 'kdeploy/output'
5
+ require_relative 'kdeploy/config/configuration'
6
+ require_relative 'kdeploy/output/output'
7
7
  require_relative 'kdeploy/banner'
8
- require_relative 'kdeploy/file_filter'
9
- require_relative 'kdeploy/dsl'
10
- require_relative 'kdeploy/executor'
11
- require_relative 'kdeploy/command_executor'
12
- require_relative 'kdeploy/output_formatter'
13
- require_relative 'kdeploy/help_formatter'
14
- require_relative 'kdeploy/runner'
15
- require_relative 'kdeploy/initializer'
16
- require_relative 'kdeploy/template'
8
+ require_relative 'kdeploy/executor/file_filter'
9
+ require_relative 'kdeploy/dsl/dsl'
10
+ require_relative 'kdeploy/executor/executor'
11
+ require_relative 'kdeploy/executor/command_executor'
12
+ require_relative 'kdeploy/output/output_formatter'
13
+ require_relative 'kdeploy/cli/help_formatter'
14
+ require_relative 'kdeploy/runner/runner'
15
+ require_relative 'kdeploy/dsl/initializer'
16
+ require_relative 'kdeploy/template/template'
17
17
  require_relative 'kdeploy/post_install'
18
- require_relative 'kdeploy/cli'
18
+ require_relative 'kdeploy/cli/cli'
19
19
 
20
20
  # Kdeploy - A lightweight agentless deployment automation tool
21
21
  module Kdeploy
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.3.1
4
+ version: 1.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kk
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-02-03 00:00:00.000000000 Z
11
+ date: 2026-07-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bcrypt_pbkdf
@@ -160,29 +160,28 @@ extensions:
160
160
  - ext/mkrf_conf.rb
161
161
  extra_rdoc_files: []
162
162
  files:
163
- - AGENTS.md
164
163
  - README.md
165
164
  - README_EN.md
166
165
  - exe/kdeploy
167
166
  - ext/mkrf_conf.rb
168
167
  - lib/kdeploy.rb
169
168
  - lib/kdeploy/banner.rb
170
- - lib/kdeploy/cli.rb
171
- - lib/kdeploy/command_executor.rb
169
+ - lib/kdeploy/cli/cli.rb
170
+ - lib/kdeploy/cli/help_formatter.rb
172
171
  - lib/kdeploy/completions/kdeploy.bash
173
172
  - lib/kdeploy/completions/kdeploy.zsh
174
- - lib/kdeploy/configuration.rb
175
- - lib/kdeploy/dsl.rb
173
+ - lib/kdeploy/config/configuration.rb
174
+ - lib/kdeploy/dsl/dsl.rb
175
+ - lib/kdeploy/dsl/initializer.rb
176
176
  - lib/kdeploy/errors.rb
177
- - lib/kdeploy/executor.rb
178
- - lib/kdeploy/file_filter.rb
179
- - lib/kdeploy/help_formatter.rb
180
- - lib/kdeploy/initializer.rb
181
- - lib/kdeploy/output.rb
182
- - lib/kdeploy/output_formatter.rb
177
+ - lib/kdeploy/executor/command_executor.rb
178
+ - lib/kdeploy/executor/executor.rb
179
+ - lib/kdeploy/executor/file_filter.rb
180
+ - lib/kdeploy/output/output.rb
181
+ - lib/kdeploy/output/output_formatter.rb
183
182
  - lib/kdeploy/post_install.rb
184
- - lib/kdeploy/runner.rb
185
- - lib/kdeploy/template.rb
183
+ - lib/kdeploy/runner/runner.rb
184
+ - lib/kdeploy/template/template.rb
186
185
  - lib/kdeploy/version.rb
187
186
  homepage: https://github.com/kevin197011/kdeploy
188
187
  licenses:
data/AGENTS.md DELETED
@@ -1,18 +0,0 @@
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 -->
@@ -1,104 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kdeploy
4
- # Executes a single command and records execution time
5
- class CommandExecutor
6
- def initialize(executor, output, debug: false, retries: 0, retry_delay: 1, retry_on_nonzero: false)
7
- @executor = executor
8
- @output = output
9
- @debug = debug
10
- @retries = retries.to_i
11
- @retry_delay = retry_delay.to_f
12
- @retry_on_nonzero = retry_on_nonzero
13
- end
14
-
15
- def execute_run(command, _host_name)
16
- cmd = command[:command]
17
- use_sudo = command[:sudo]
18
-
19
- result, duration = measure_time do
20
- with_retries { @executor.execute(cmd, use_sudo: use_sudo) }
21
- end
22
-
23
- { command: cmd, output: result, duration: duration, type: :run }
24
- end
25
-
26
- def execute_upload(command, _host_name)
27
- _result, duration = measure_time do
28
- with_retries { @executor.upload(command[:source], command[:destination]) }
29
- end
30
- {
31
- command: "upload: #{command[:source]} -> #{command[:destination]}",
32
- duration: duration,
33
- type: :upload
34
- }
35
- end
36
-
37
- def execute_upload_template(command, _host_name)
38
- _result, duration = measure_time do
39
- with_retries do
40
- @executor.upload_template(command[:source], command[:destination], command[:variables])
41
- end
42
- end
43
- {
44
- command: "upload_template: #{command[:source]} -> #{command[:destination]}",
45
- duration: duration,
46
- type: :upload_template
47
- }
48
- end
49
-
50
- def execute_sync(command, _host_name)
51
- source = command[:source]
52
- destination = command[:destination]
53
-
54
- result, duration = measure_time do
55
- with_retries do
56
- @executor.sync_directory(
57
- source,
58
- destination,
59
- ignore: command[:ignore] || [],
60
- exclude: command[:exclude] || [],
61
- delete: command[:delete] || false
62
- )
63
- end
64
- end
65
-
66
- build_sync_result(source, destination, result, duration)
67
- end
68
-
69
- private
70
-
71
- def build_sync_result(source, destination, result, duration)
72
- {
73
- command: "sync: #{source} -> #{destination}",
74
- duration: duration,
75
- type: :sync,
76
- result: result,
77
- uploaded: result[:uploaded],
78
- deleted: result[:deleted],
79
- total: result[:total]
80
- }
81
- end
82
-
83
- def measure_time
84
- start_time = Time.now
85
- result = yield
86
- duration = Time.now - start_time
87
- [result, duration]
88
- end
89
-
90
- def with_retries
91
- attempts = 0
92
- begin
93
- attempts += 1
94
- yield
95
- rescue SSHError, SCPError, TemplateError => e
96
- raise if e.is_a?(SSHError) && e.exit_status && !@retry_on_nonzero
97
- raise if attempts > (@retries + 1)
98
-
99
- sleep(@retry_delay) if @retry_delay.positive?
100
- retry
101
- end
102
- end
103
- end
104
- end
File without changes