kdeploy 1.3.5 → 1.4.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 +4 -4
- data/CHANGELOG.md +80 -0
- data/CONTRIBUTING.md +35 -0
- data/LICENSE +21 -0
- data/README.md +17 -7
- data/README_EN.md +18 -7
- data/SECURITY.md +37 -0
- data/lib/kdeploy/cli/cli.rb +64 -9
- data/lib/kdeploy/cli/help_formatter.rb +6 -0
- data/lib/kdeploy/config/configuration.rb +7 -2
- data/lib/kdeploy/dsl/dsl.rb +211 -110
- data/lib/kdeploy/dsl/initializer.rb +20 -0
- data/lib/kdeploy/executor/command_executor.rb +77 -1
- data/lib/kdeploy/executor/executor.rb +91 -17
- data/lib/kdeploy/output/output_formatter.rb +12 -3
- data/lib/kdeploy/runner/runner.rb +74 -22
- data/lib/kdeploy/template/template.rb +2 -1
- data/lib/kdeploy/version.rb +1 -1
- metadata +9 -3
|
@@ -21,7 +21,8 @@ module Kdeploy
|
|
|
21
21
|
@port = host_config[:port] # Added custom port support
|
|
22
22
|
@use_sudo = host_config[:use_sudo] || false
|
|
23
23
|
@sudo_password = host_config[:sudo_password]
|
|
24
|
-
|
|
24
|
+
raw_base = host_config[:base_dir]
|
|
25
|
+
@base_dir = raw_base ? File.expand_path(raw_base) : nil
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
def execute(command, use_sudo: nil)
|
|
@@ -29,7 +30,7 @@ module Kdeploy
|
|
|
29
30
|
command = wrap_with_sudo(command) if use_sudo
|
|
30
31
|
|
|
31
32
|
Net::SSH.start(@ip, @user, ssh_options) do |ssh|
|
|
32
|
-
execute_command_on_ssh(ssh, command)
|
|
33
|
+
with_active_ssh(ssh) { execute_command_on_ssh(ssh, command) }
|
|
33
34
|
end
|
|
34
35
|
rescue Net::SSH::AuthenticationFailed => e
|
|
35
36
|
raise SSHError.new("SSH authentication failed: #{e.message}", e)
|
|
@@ -37,6 +38,36 @@ module Kdeploy
|
|
|
37
38
|
raise SSHError.new("SSH execution failed: #{e.message}", e)
|
|
38
39
|
end
|
|
39
40
|
|
|
41
|
+
# Best-effort close of the in-flight SSH session (step/host timeout).
|
|
42
|
+
def host_ip
|
|
43
|
+
@ip
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def host_name
|
|
47
|
+
@host
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def abort!
|
|
51
|
+
ssh = @active_ssh
|
|
52
|
+
return unless ssh
|
|
53
|
+
|
|
54
|
+
warn "Warning: aborting SSH session for #{@host} (#{@ip})"
|
|
55
|
+
begin
|
|
56
|
+
ssh.close
|
|
57
|
+
rescue StandardError
|
|
58
|
+
# ignore
|
|
59
|
+
ensure
|
|
60
|
+
@active_ssh = nil
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def with_active_ssh(ssh)
|
|
65
|
+
@active_ssh = ssh
|
|
66
|
+
yield
|
|
67
|
+
ensure
|
|
68
|
+
@active_ssh = nil if @active_ssh.equal?(ssh)
|
|
69
|
+
end
|
|
70
|
+
|
|
40
71
|
def execute_command_on_ssh(ssh, command)
|
|
41
72
|
stdout = String.new
|
|
42
73
|
stderr = String.new
|
|
@@ -53,6 +84,7 @@ module Kdeploy
|
|
|
53
84
|
end
|
|
54
85
|
end
|
|
55
86
|
ssh.loop
|
|
87
|
+
raise_missing_exit_status!(command, stdout, stderr) if exit_status.nil?
|
|
56
88
|
raise_nonzero_exit!(command, exit_status, stdout, stderr)
|
|
57
89
|
build_command_result(stdout, stderr, command, exit_status)
|
|
58
90
|
end
|
|
@@ -76,8 +108,18 @@ module Kdeploy
|
|
|
76
108
|
}
|
|
77
109
|
end
|
|
78
110
|
|
|
111
|
+
def raise_missing_exit_status!(command, stdout, stderr)
|
|
112
|
+
raise SSHError.new(
|
|
113
|
+
'Command completed without exit status',
|
|
114
|
+
nil,
|
|
115
|
+
command: command,
|
|
116
|
+
exit_status: nil,
|
|
117
|
+
stdout: stdout.strip,
|
|
118
|
+
stderr: stderr.strip
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
79
122
|
def raise_nonzero_exit!(command, exit_status, stdout, stderr)
|
|
80
|
-
return if exit_status.nil?
|
|
81
123
|
return if exit_status.zero?
|
|
82
124
|
|
|
83
125
|
raise SSHError.new(
|
|
@@ -101,7 +143,9 @@ module Kdeploy
|
|
|
101
143
|
upload_with_sudo(resolved_source, destination)
|
|
102
144
|
else
|
|
103
145
|
Net::SCP.start(@ip, @user, ssh_options) do |scp|
|
|
104
|
-
scp.
|
|
146
|
+
with_active_ssh(scp.respond_to?(:session) ? scp.session : nil) do
|
|
147
|
+
scp.upload!(resolved_source, destination)
|
|
148
|
+
end
|
|
105
149
|
end
|
|
106
150
|
end
|
|
107
151
|
rescue StandardError => e
|
|
@@ -112,6 +156,8 @@ module Kdeploy
|
|
|
112
156
|
# Resolve relative paths relative to base_dir
|
|
113
157
|
resolved_source = resolve_path(source)
|
|
114
158
|
Template.render_and_upload(self, resolved_source, destination, variables)
|
|
159
|
+
rescue ArgumentError, Errno::ENOENT, FileNotFoundError
|
|
160
|
+
raise
|
|
115
161
|
rescue StandardError => e
|
|
116
162
|
raise TemplateError.new("Template upload failed: #{e.message}", e)
|
|
117
163
|
end
|
|
@@ -140,6 +186,8 @@ module Kdeploy
|
|
|
140
186
|
use_sudo: use_sudo
|
|
141
187
|
)
|
|
142
188
|
return rsync_result if rsync_result
|
|
189
|
+
|
|
190
|
+
warn 'Warning: rsync fast path unavailable; falling back to SCP sync'
|
|
143
191
|
end
|
|
144
192
|
|
|
145
193
|
# Collect files to sync
|
|
@@ -163,6 +211,8 @@ module Kdeploy
|
|
|
163
211
|
deleted: deleted_count,
|
|
164
212
|
total: files_to_sync.size
|
|
165
213
|
}
|
|
214
|
+
rescue FileNotFoundError, ArgumentError
|
|
215
|
+
raise
|
|
166
216
|
rescue StandardError => e
|
|
167
217
|
raise SCPError.new("Directory sync failed: #{e.message}", e)
|
|
168
218
|
end
|
|
@@ -175,16 +225,18 @@ module Kdeploy
|
|
|
175
225
|
|
|
176
226
|
# Upload to temp location first
|
|
177
227
|
Net::SCP.start(@ip, @user, ssh_options) do |scp|
|
|
178
|
-
scp.
|
|
228
|
+
with_active_ssh(scp.respond_to?(:session) ? scp.session : nil) do
|
|
229
|
+
scp.upload!(source, temp_dest)
|
|
230
|
+
end
|
|
179
231
|
end
|
|
180
232
|
|
|
181
233
|
# Move to final destination with sudo
|
|
182
|
-
move_command = "mv #{temp_dest} #{destination}"
|
|
234
|
+
move_command = "mv #{Shellwords.escape(temp_dest)} #{Shellwords.escape(destination)}"
|
|
183
235
|
execute(move_command, use_sudo: true)
|
|
184
236
|
rescue StandardError => e
|
|
185
237
|
# Try to clean up temp file if it exists
|
|
186
238
|
begin
|
|
187
|
-
execute("rm -f #{temp_dest}", use_sudo: false) if defined?(temp_dest)
|
|
239
|
+
execute("rm -f #{Shellwords.escape(temp_dest)}", use_sudo: false) if defined?(temp_dest)
|
|
188
240
|
rescue StandardError
|
|
189
241
|
# Ignore cleanup errors
|
|
190
242
|
end
|
|
@@ -257,7 +309,7 @@ module Kdeploy
|
|
|
257
309
|
sudo_flag,
|
|
258
310
|
"-e \"#{ssh_cmd}\"",
|
|
259
311
|
"#{Shellwords.escape(source)}/",
|
|
260
|
-
"#{@user}@#{@ip}:#{Shellwords.escape(destination)}/"
|
|
312
|
+
"#{Shellwords.escape(@user)}@#{Shellwords.escape(@ip)}:#{Shellwords.escape(destination)}/"
|
|
261
313
|
].reject(&:empty?)
|
|
262
314
|
parts.join(' ')
|
|
263
315
|
end
|
|
@@ -314,29 +366,43 @@ module Kdeploy
|
|
|
314
366
|
# Do not add sudo again if command already starts with sudo
|
|
315
367
|
return command if command.strip.start_with?('sudo')
|
|
316
368
|
|
|
317
|
-
# Wrap multi-line/control/compound commands so sudo applies to the full block
|
|
369
|
+
# Wrap multi-line/control/compound commands so sudo applies to the full block.
|
|
370
|
+
# Encode payload as base64 so local shell quoting cannot break on quotes/newlines.
|
|
318
371
|
needs_wrap = command.include?("\n") ||
|
|
319
372
|
command.match?(/\b(if|for|while|case|function)\b/) ||
|
|
320
373
|
command.match?(/\s(&&|\|\||;)\s/)
|
|
321
374
|
|
|
322
375
|
if needs_wrap
|
|
323
|
-
|
|
324
|
-
escaped_command = command.gsub("'", "'\"'\"'")
|
|
376
|
+
remote = base64_bash_command(command)
|
|
325
377
|
if @sudo_password
|
|
326
|
-
|
|
327
|
-
"echo '#{escaped_password}' | sudo -S bash -c '#{escaped_command}'"
|
|
378
|
+
"#{sudo_password_prefix}sudo -S #{remote}"
|
|
328
379
|
else
|
|
329
|
-
"sudo
|
|
380
|
+
"sudo #{remote}"
|
|
330
381
|
end
|
|
331
382
|
elsif @sudo_password
|
|
332
|
-
#
|
|
333
|
-
escaped_password = @sudo_password.gsub('\'', "'\"'\"'").gsub('$', '\\$').gsub('`', '\\`')
|
|
334
|
-
"echo '#{escaped_password}' | sudo -S #{command}"
|
|
383
|
+
"#{sudo_password_prefix}sudo -S #{command}"
|
|
335
384
|
else
|
|
336
385
|
"sudo #{command}"
|
|
337
386
|
end
|
|
338
387
|
end
|
|
339
388
|
|
|
389
|
+
def base64_bash_command(command)
|
|
390
|
+
encoded = [command].pack('m0')
|
|
391
|
+
"bash -c #{Shellwords.escape("echo #{encoded} | base64 -d | bash")}"
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def sudo_password_prefix
|
|
395
|
+
validate_sudo_password!
|
|
396
|
+
# printf + Shellwords avoids echo/newline injection into the local shell.
|
|
397
|
+
"printf '%s\\n' #{Shellwords.escape(@sudo_password)} | "
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def validate_sudo_password!
|
|
401
|
+
return if @sudo_password.nil?
|
|
402
|
+
|
|
403
|
+
raise ArgumentError, 'sudo_password must not contain CR, LF, or NUL' if @sudo_password.match?(/[\r\n\0]/)
|
|
404
|
+
end
|
|
405
|
+
|
|
340
406
|
def collect_files_to_sync(source_dir, filter)
|
|
341
407
|
files = []
|
|
342
408
|
source_path = Pathname.new(source_dir)
|
|
@@ -445,6 +511,14 @@ module Kdeploy
|
|
|
445
511
|
# Ignore deletion errors
|
|
446
512
|
end
|
|
447
513
|
|
|
514
|
+
# Best-effort: remove empty directories left behind (not the sync root).
|
|
515
|
+
prune = "find #{destination_dir.shellescape} -mindepth 1 -depth -type d -empty -delete 2>/dev/null || true"
|
|
516
|
+
begin
|
|
517
|
+
execute(prune, use_sudo: use_sudo)
|
|
518
|
+
rescue StandardError
|
|
519
|
+
# Ignore prune errors
|
|
520
|
+
end
|
|
521
|
+
|
|
448
522
|
deleted_count
|
|
449
523
|
end
|
|
450
524
|
end
|
|
@@ -157,9 +157,18 @@ module Kdeploy
|
|
|
157
157
|
when :upload_template
|
|
158
158
|
"#{@pastel.blue('>')} Template: #{command[:source]} -> #{command[:destination]}"
|
|
159
159
|
when :sync
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
"
|
|
160
|
+
extras = []
|
|
161
|
+
extras << "ignore: #{command[:ignore].join(', ')}" if command[:ignore]&.any?
|
|
162
|
+
extras << "exclude: #{command[:exclude].join(', ')}" if command[:exclude]&.any?
|
|
163
|
+
extras << 'delete: true' if command[:delete]
|
|
164
|
+
extras << 'fast: true' if command[:fast]
|
|
165
|
+
extras << "parallel: #{command[:parallel]}" if command[:parallel]
|
|
166
|
+
suffix = extras.empty? ? '' : " (#{extras.join(', ')})"
|
|
167
|
+
"#{@pastel.blue('>')} Sync: #{command[:source]} -> #{command[:destination]}#{suffix}"
|
|
168
|
+
when :wait_port
|
|
169
|
+
"#{@pastel.magenta('>')} wait_port: #{command[:port]} (timeout: #{command[:timeout]})"
|
|
170
|
+
when :wait_http
|
|
171
|
+
"#{@pastel.magenta('>')} wait_http: #{command[:url]} (code: #{command[:code]}, timeout: #{command[:timeout]})"
|
|
163
172
|
else
|
|
164
173
|
"#{@pastel.blue('>')} #{command[:type]}: #{command}"
|
|
165
174
|
end
|
|
@@ -26,6 +26,7 @@ module Kdeploy
|
|
|
26
26
|
@retry_policy = retry_policy
|
|
27
27
|
@pool = Concurrent::FixedThreadPool.new(@parallel)
|
|
28
28
|
@results = Concurrent::Hash.new
|
|
29
|
+
@results_mutex = Mutex.new
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
def run(task_name)
|
|
@@ -66,11 +67,16 @@ module Kdeploy
|
|
|
66
67
|
progressed = true
|
|
67
68
|
elsif timeout_exceeded?(started_at, now)
|
|
68
69
|
future.cancel if future.respond_to?(:cancel)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
begin
|
|
71
|
+
meta[:executor_ref]&.get&.abort!
|
|
72
|
+
rescue StandardError
|
|
73
|
+
# ignore abort errors
|
|
74
|
+
end
|
|
75
|
+
store_result_once(host_name, {
|
|
76
|
+
status: :failed,
|
|
77
|
+
error: "execution timeout after #{@host_timeout}s",
|
|
78
|
+
output: []
|
|
79
|
+
})
|
|
74
80
|
pending.delete(future)
|
|
75
81
|
progressed = true
|
|
76
82
|
end
|
|
@@ -85,22 +91,24 @@ module Kdeploy
|
|
|
85
91
|
def create_task_futures(task, task_name)
|
|
86
92
|
@future_meta = {}
|
|
87
93
|
@hosts.map do |name, config|
|
|
88
|
-
|
|
94
|
+
# Start the host-timeout clock when scheduled so pool wait cannot skip timeout.
|
|
95
|
+
started_at = Concurrent::AtomicReference.new(Time.now)
|
|
96
|
+
executor_ref = Concurrent::AtomicReference.new(nil)
|
|
89
97
|
future = Concurrent::Future.execute(executor: @pool) do
|
|
90
|
-
|
|
91
|
-
execute_task_for_host(name, config, task, task_name)
|
|
98
|
+
execute_task_for_host(name, config, task, task_name, executor_ref)
|
|
92
99
|
end
|
|
93
|
-
@future_meta[future] = { host_name: name, started_at: started_at }
|
|
100
|
+
@future_meta[future] = { host_name: name, started_at: started_at, executor_ref: executor_ref }
|
|
94
101
|
future
|
|
95
102
|
end
|
|
96
103
|
end
|
|
97
104
|
|
|
98
105
|
private
|
|
99
106
|
|
|
100
|
-
def execute_task_for_host(name, config, task, task_name)
|
|
107
|
+
def execute_task_for_host(name, config, task, task_name, executor_ref = nil)
|
|
101
108
|
# Add base_dir to config for path resolution
|
|
102
109
|
config_with_base_dir = config.merge(base_dir: @base_dir)
|
|
103
110
|
executor = Executor.new(config_with_base_dir)
|
|
111
|
+
executor_ref&.set(executor)
|
|
104
112
|
command_executor = CommandExecutor.new(
|
|
105
113
|
executor,
|
|
106
114
|
@output,
|
|
@@ -126,7 +134,9 @@ module Kdeploy
|
|
|
126
134
|
end
|
|
127
135
|
|
|
128
136
|
def execute_commands(task, command_executor, name, result, task_name)
|
|
129
|
-
|
|
137
|
+
plan = Kdeploy::DSL.normalize_plan(task[:block].call)
|
|
138
|
+
commands = plan[:commands]
|
|
139
|
+
on_failure = plan[:on_failure]
|
|
130
140
|
|
|
131
141
|
commands.each do |command|
|
|
132
142
|
step_result = execute_command(command_executor, command, name)
|
|
@@ -142,26 +152,46 @@ module Kdeploy
|
|
|
142
152
|
error: "#{e.class}: #{e.message}",
|
|
143
153
|
output: error_output_for_step(e)
|
|
144
154
|
}
|
|
155
|
+
run_on_failure(on_failure, command_executor, name, result)
|
|
145
156
|
break
|
|
146
157
|
end
|
|
147
158
|
end
|
|
148
159
|
|
|
160
|
+
def run_on_failure(on_failure, command_executor, name, result)
|
|
161
|
+
return if on_failure.nil? || on_failure.empty?
|
|
162
|
+
|
|
163
|
+
on_failure.each do |command|
|
|
164
|
+
step_result = execute_command(command_executor, command, name)
|
|
165
|
+
step_result = step_result.merge(on_failure: true)
|
|
166
|
+
result[:output] << step_result
|
|
167
|
+
rescue StandardError => e
|
|
168
|
+
result[:output] << {
|
|
169
|
+
type: command[:type],
|
|
170
|
+
command: step_command_string(command),
|
|
171
|
+
duration: 0.0,
|
|
172
|
+
on_failure: true,
|
|
173
|
+
error: "#{e.class}: #{e.message}",
|
|
174
|
+
output: error_output_for_step(e)
|
|
175
|
+
}
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
149
179
|
def collect_future_result(future, host_name)
|
|
150
|
-
return if
|
|
180
|
+
return if result_recorded?(host_name)
|
|
151
181
|
|
|
152
182
|
begin
|
|
153
183
|
future_result = future.value
|
|
154
184
|
if future_result.nil?
|
|
155
|
-
|
|
185
|
+
store_result_once(host_name, { status: :unknown, error: 'Future returned nil', output: [] })
|
|
156
186
|
elsif future_result.is_a?(Array) && future_result.length == 2
|
|
157
187
|
name, result = future_result
|
|
158
|
-
|
|
188
|
+
store_result_once(name, result)
|
|
159
189
|
else
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
190
|
+
store_result_once(host_name, {
|
|
191
|
+
status: :unknown,
|
|
192
|
+
error: "Unexpected result format: #{future_result.class}",
|
|
193
|
+
output: []
|
|
194
|
+
})
|
|
165
195
|
end
|
|
166
196
|
|
|
167
197
|
if future.rejected?
|
|
@@ -170,12 +200,22 @@ module Kdeploy
|
|
|
170
200
|
rescue StandardError
|
|
171
201
|
'Unknown error'
|
|
172
202
|
end
|
|
173
|
-
|
|
203
|
+
store_result_once(host_name, { status: :failed, error: error, output: [] })
|
|
174
204
|
end
|
|
175
205
|
rescue StandardError => e
|
|
176
|
-
|
|
206
|
+
store_result_once(host_name, { status: :failed, error: "#{e.class}: #{e.message}", output: [] })
|
|
177
207
|
ensure
|
|
178
|
-
|
|
208
|
+
store_result_once(host_name, { status: :unknown, error: 'No result collected', output: [] })
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def result_recorded?(host_name)
|
|
213
|
+
@results_mutex.synchronize { @results.key?(host_name) }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def store_result_once(host_name, result)
|
|
217
|
+
@results_mutex.synchronize do
|
|
218
|
+
@results[host_name] ||= result
|
|
179
219
|
end
|
|
180
220
|
end
|
|
181
221
|
|
|
@@ -225,6 +265,10 @@ module Kdeploy
|
|
|
225
265
|
command_executor.execute_upload_template(command, host_name)
|
|
226
266
|
when :sync
|
|
227
267
|
command_executor.execute_sync(command, host_name)
|
|
268
|
+
when :wait_port
|
|
269
|
+
command_executor.execute_wait_port(command, host_name)
|
|
270
|
+
when :wait_http
|
|
271
|
+
command_executor.execute_wait_http(command, host_name)
|
|
228
272
|
else
|
|
229
273
|
raise ConfigurationError, "Unknown command type: #{command[:type]}"
|
|
230
274
|
end
|
|
@@ -240,6 +284,10 @@ module Kdeploy
|
|
|
240
284
|
"upload_template: #{command[:source]} -> #{command[:destination]}"
|
|
241
285
|
when :sync
|
|
242
286
|
"sync: #{command[:source]} -> #{command[:destination]}"
|
|
287
|
+
when :wait_port
|
|
288
|
+
"wait_port: #{command[:port]}"
|
|
289
|
+
when :wait_http
|
|
290
|
+
"wait_http: #{command[:url]}"
|
|
243
291
|
else
|
|
244
292
|
command[:type].to_s
|
|
245
293
|
end
|
|
@@ -256,6 +304,10 @@ module Kdeploy
|
|
|
256
304
|
"upload_template: #{command[:source]} -> #{command[:destination]}"
|
|
257
305
|
when :sync
|
|
258
306
|
"sync: #{command[:source]} -> #{command[:destination]}"
|
|
307
|
+
when :wait_port
|
|
308
|
+
"wait_port: #{command[:port]}"
|
|
309
|
+
when :wait_http
|
|
310
|
+
"wait_http: #{command[:url]}"
|
|
259
311
|
else
|
|
260
312
|
command[:type].to_s
|
|
261
313
|
end
|
|
@@ -51,7 +51,8 @@ module Kdeploy
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def self.extract_template_identifiers(template_content)
|
|
54
|
-
|
|
54
|
+
# Match <%= name %> and <%= @name %>; still ignores method calls / hash access.
|
|
55
|
+
identifiers = template_content.scan(/<%=\s*@?([a-zA-Z_]\w*)/).flatten
|
|
55
56
|
identifiers.uniq - ruby_keywords
|
|
56
57
|
end
|
|
57
58
|
|
data/lib/kdeploy/version.rb
CHANGED
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.
|
|
4
|
+
version: 1.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kk
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bcrypt_pbkdf
|
|
@@ -160,8 +160,12 @@ extensions:
|
|
|
160
160
|
- ext/mkrf_conf.rb
|
|
161
161
|
extra_rdoc_files: []
|
|
162
162
|
files:
|
|
163
|
+
- CHANGELOG.md
|
|
164
|
+
- CONTRIBUTING.md
|
|
165
|
+
- LICENSE
|
|
163
166
|
- README.md
|
|
164
167
|
- README_EN.md
|
|
168
|
+
- SECURITY.md
|
|
165
169
|
- exe/kdeploy
|
|
166
170
|
- ext/mkrf_conf.rb
|
|
167
171
|
- lib/kdeploy.rb
|
|
@@ -188,8 +192,10 @@ licenses:
|
|
|
188
192
|
- MIT
|
|
189
193
|
metadata:
|
|
190
194
|
homepage_uri: https://github.com/kevin197011/kdeploy
|
|
191
|
-
source_code_uri: https://github.com/kevin197011/kdeploy
|
|
195
|
+
source_code_uri: https://github.com/kevin197011/kdeploy
|
|
192
196
|
changelog_uri: https://github.com/kevin197011/kdeploy/blob/main/CHANGELOG.md
|
|
197
|
+
bug_tracker_uri: https://github.com/kevin197011/kdeploy/issues
|
|
198
|
+
documentation_uri: https://github.com/kevin197011/kdeploy/blob/main/docs/REQUIREMENTS.md
|
|
193
199
|
rubygems_mfa_required: 'true'
|
|
194
200
|
post_install_message: "\U0001F389 Thanks for installing kdeploy!\n\nShell completion
|
|
195
201
|
will be configured automatically.\nYou may need to restart your shell or run:\n
|