kdeploy 1.2.23 → 1.2.28
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/README.md +61 -1
- data/lib/kdeploy/cli.rb +2 -0
- data/lib/kdeploy/command_executor.rb +32 -0
- data/lib/kdeploy/command_grouper.rb +4 -0
- data/lib/kdeploy/dsl.rb +50 -0
- data/lib/kdeploy/executor.rb +110 -0
- data/lib/kdeploy/file_filter.rb +72 -0
- data/lib/kdeploy/initializer.rb +17 -0
- data/lib/kdeploy/output_formatter.rb +34 -0
- data/lib/kdeploy/runner.rb +2 -0
- data/lib/kdeploy/version.rb +1 -1
- data/lib/kdeploy.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6e6d5731024f6c6038b668226945d6360dcd2e6abab1e68a54872b0e815bacd0
|
|
4
|
+
data.tar.gz: 9c8505220dfb4999a9fa62a8607428b880b7bd954c7ecc9707b6dd5a72c65e25
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aa53413eb0414244474a7f1e838074ecdefa34ae7e146a7711abb04317babdeb1ae6b1492a5f33c2357fb311ab58657f19e4155eace3ad045e338f2b3891938b
|
|
7
|
+
data.tar.gz: 26d4cd0add001ff1877f8fdc013f6ee89b9eb0d1fe6951cc197d202167613c9f69e85d8af10512cce08d7c76cdec1de5b208e2f595b4bb15a7a9754ecd680e4d
|
data/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|_| |___/
|
|
10
10
|
|
|
11
11
|
⚡ 轻量级无代理部署工具
|
|
12
|
-
🚀
|
|
12
|
+
🚀 自动部署,轻松扩展
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
一个用 Ruby 编写的轻量级、无代理的部署自动化工具。Kdeploy 使您能够使用 SSH 在多个服务器上部署应用程序、管理配置和执行任务,而无需在目标机器上安装任何代理或守护进程。
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
- 📝 **优雅的 Ruby DSL**: 简单而富有表现力的任务定义语法
|
|
45
45
|
- 🚀 **并发执行**: 跨多个主机的高效并行任务处理
|
|
46
46
|
- 📤 **文件上传支持**: 通过 SCP 轻松部署文件和模板
|
|
47
|
+
- 📁 **目录同步功能**: 递归同步目录,支持文件过滤和删除多余文件
|
|
47
48
|
- 📊 **任务状态跟踪**: 实时执行监控,提供详细输出
|
|
48
49
|
- 🔄 **ERB 模板支持**: 支持变量替换的动态配置生成
|
|
49
50
|
- 🎯 **基于角色的部署**: 针对特定服务器角色进行有组织的部署
|
|
@@ -472,6 +473,48 @@ upload_template "./config/nginx.conf.erb", "/etc/nginx/nginx.conf",
|
|
|
472
473
|
- `destination`: 远程文件路径
|
|
473
474
|
- `variables`: 用于模板渲染的变量哈希
|
|
474
475
|
|
|
476
|
+
#### `sync` - 同步目录
|
|
477
|
+
|
|
478
|
+
递归同步本地目录到远程服务器,支持文件过滤和删除多余文件。
|
|
479
|
+
|
|
480
|
+
```ruby
|
|
481
|
+
# 基本同步
|
|
482
|
+
sync "./app", "/var/www/app"
|
|
483
|
+
|
|
484
|
+
# 同步并忽略特定文件/目录
|
|
485
|
+
sync "./app", "/var/www/app",
|
|
486
|
+
ignore: [".git", "*.log", "node_modules", "*.tmp"]
|
|
487
|
+
|
|
488
|
+
# 同步并删除远程多余文件
|
|
489
|
+
sync "./app", "/var/www/app",
|
|
490
|
+
ignore: [".git", "*.log"],
|
|
491
|
+
delete: true
|
|
492
|
+
|
|
493
|
+
# 排除特定文件(与 ignore 相同,但语义更清晰)
|
|
494
|
+
sync "./config", "/etc/app",
|
|
495
|
+
exclude: ["*.example", "*.bak", ".env.local"]
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
**参数:**
|
|
499
|
+
- `source`: 本地源目录路径
|
|
500
|
+
- `destination`: 远程目标目录路径
|
|
501
|
+
- `ignore`: 要忽略的文件/目录模式数组(支持 .gitignore 风格的通配符)
|
|
502
|
+
- `exclude`: 与 `ignore` 相同,用于语义清晰
|
|
503
|
+
- `delete`: 布尔值,是否删除远程目录中不存在于源目录的文件(默认: false)
|
|
504
|
+
|
|
505
|
+
**忽略模式支持:**
|
|
506
|
+
- `*.log` - 匹配所有 .log 文件
|
|
507
|
+
- `node_modules` - 匹配 node_modules 目录或文件
|
|
508
|
+
- `**/*.tmp` - 递归匹配所有 .tmp 文件
|
|
509
|
+
- `.git` - 匹配 .git 目录
|
|
510
|
+
- `config/*.local` - 匹配 config 目录下的所有 .local 文件
|
|
511
|
+
|
|
512
|
+
**使用场景:**
|
|
513
|
+
- 部署应用程序代码
|
|
514
|
+
- 同步配置文件目录
|
|
515
|
+
- 同步静态资源文件
|
|
516
|
+
- 保持本地和远程目录结构一致
|
|
517
|
+
|
|
475
518
|
### 模板支持
|
|
476
519
|
|
|
477
520
|
Kdeploy 支持 ERB(嵌入式 Ruby)模板,用于动态配置生成。
|
|
@@ -1005,6 +1048,23 @@ task :update_config, roles: :web do
|
|
|
1005
1048
|
end
|
|
1006
1049
|
```
|
|
1007
1050
|
|
|
1051
|
+
#### 目录同步部署
|
|
1052
|
+
|
|
1053
|
+
```ruby
|
|
1054
|
+
task :deploy_app, roles: :web do
|
|
1055
|
+
# 同步应用程序代码,忽略开发文件
|
|
1056
|
+
sync "./app", "/var/www/app",
|
|
1057
|
+
ignore: [".git", "*.log", "node_modules", ".env.local", "*.tmp"],
|
|
1058
|
+
delete: true
|
|
1059
|
+
|
|
1060
|
+
# 同步配置文件
|
|
1061
|
+
sync "./config", "/etc/app",
|
|
1062
|
+
exclude: ["*.example", "*.bak"]
|
|
1063
|
+
|
|
1064
|
+
run "sudo systemctl restart app"
|
|
1065
|
+
end
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1008
1068
|
## 📝 许可证
|
|
1009
1069
|
|
|
1010
1070
|
该 gem 在 [MIT 许可证](https://opensource.org/licenses/MIT) 条款下作为开源提供。
|
data/lib/kdeploy/cli.rb
CHANGED
|
@@ -162,6 +162,8 @@ module Kdeploy
|
|
|
162
162
|
formatter.format_upload_steps(steps, shown)
|
|
163
163
|
when :upload_template
|
|
164
164
|
formatter.format_template_steps(steps, shown)
|
|
165
|
+
when :sync
|
|
166
|
+
formatter.format_sync_steps(steps, shown)
|
|
165
167
|
when :run
|
|
166
168
|
formatter.format_run_steps(steps, shown)
|
|
167
169
|
else
|
|
@@ -53,6 +53,38 @@ module Kdeploy
|
|
|
53
53
|
}
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
def execute_sync(command, host_name)
|
|
57
|
+
source = command[:source]
|
|
58
|
+
destination = command[:destination]
|
|
59
|
+
ignore = command[:ignore] || []
|
|
60
|
+
exclude = command[:exclude] || []
|
|
61
|
+
delete = command[:delete] || false
|
|
62
|
+
|
|
63
|
+
description = "sync: #{source} -> #{destination}"
|
|
64
|
+
description += " (delete: #{delete})" if delete
|
|
65
|
+
show_command_header(host_name, :sync, description)
|
|
66
|
+
|
|
67
|
+
result, duration = measure_time do
|
|
68
|
+
@executor.sync_directory(
|
|
69
|
+
source,
|
|
70
|
+
destination,
|
|
71
|
+
ignore: ignore,
|
|
72
|
+
exclude: exclude,
|
|
73
|
+
delete: delete
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
{
|
|
78
|
+
command: "sync: #{source} -> #{destination}",
|
|
79
|
+
duration: duration,
|
|
80
|
+
type: :sync,
|
|
81
|
+
result: result,
|
|
82
|
+
uploaded: result[:uploaded],
|
|
83
|
+
deleted: result[:deleted],
|
|
84
|
+
total: result[:total]
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
56
88
|
private
|
|
57
89
|
|
|
58
90
|
def measure_time
|
|
@@ -13,6 +13,8 @@ module Kdeploy
|
|
|
13
13
|
case cmd[:type]
|
|
14
14
|
when :upload, :upload_template
|
|
15
15
|
"#{cmd[:type]}_#{cmd[:source]}"
|
|
16
|
+
when :sync
|
|
17
|
+
"#{cmd[:type]}_#{cmd[:source]}"
|
|
16
18
|
when :run
|
|
17
19
|
"#{cmd[:type]}_#{cmd[:command].to_s.lines.first.strip}"
|
|
18
20
|
else
|
|
@@ -26,6 +28,8 @@ module Kdeploy
|
|
|
26
28
|
"upload #{command[:source]}"
|
|
27
29
|
when :upload_template
|
|
28
30
|
"template #{command[:source]}"
|
|
31
|
+
when :sync
|
|
32
|
+
"sync #{command[:source]}"
|
|
29
33
|
when :run
|
|
30
34
|
command[:command].to_s.lines.first.strip
|
|
31
35
|
else
|
data/lib/kdeploy/dsl.rb
CHANGED
|
@@ -39,6 +39,44 @@ module Kdeploy
|
|
|
39
39
|
}
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Assign task to roles or hosts after task definition
|
|
43
|
+
def assign_task(task_name, on: nil, roles: nil)
|
|
44
|
+
task = kdeploy_tasks[task_name.to_sym]
|
|
45
|
+
raise ArgumentError, "Task #{task_name} not found" unless task
|
|
46
|
+
|
|
47
|
+
task[:hosts] = normalize_hosts_option(on) if on
|
|
48
|
+
task[:roles] = normalize_roles_option(roles) if roles
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Include task file and automatically assign all tasks to specified roles or hosts
|
|
52
|
+
def include_tasks(file_path, roles: nil, on: nil)
|
|
53
|
+
# Resolve relative paths based on the caller's file location
|
|
54
|
+
unless File.absolute_path?(file_path)
|
|
55
|
+
caller_file = caller_locations(1, 1).first.path
|
|
56
|
+
base_dir = File.dirname(File.expand_path(caller_file))
|
|
57
|
+
file_path = File.expand_path(file_path, base_dir)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Store tasks before loading
|
|
61
|
+
tasks_before = kdeploy_tasks.keys
|
|
62
|
+
|
|
63
|
+
# Load the task file
|
|
64
|
+
module_eval(File.read(file_path), file_path)
|
|
65
|
+
|
|
66
|
+
# Get newly added tasks
|
|
67
|
+
tasks_after = kdeploy_tasks.keys
|
|
68
|
+
new_tasks = tasks_after - tasks_before
|
|
69
|
+
|
|
70
|
+
# Assign roles/hosts to all new tasks (only if task doesn't already have hosts/roles)
|
|
71
|
+
new_tasks.each do |task_name|
|
|
72
|
+
task = kdeploy_tasks[task_name]
|
|
73
|
+
# Only assign if task doesn't already have hosts or roles defined
|
|
74
|
+
next if task[:hosts] || task[:roles]
|
|
75
|
+
|
|
76
|
+
assign_task(task_name, roles: roles, on: on) if roles || on
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
42
80
|
def normalize_hosts_option(on)
|
|
43
81
|
return on if on.is_a?(Array)
|
|
44
82
|
|
|
@@ -83,6 +121,18 @@ module Kdeploy
|
|
|
83
121
|
}
|
|
84
122
|
end
|
|
85
123
|
|
|
124
|
+
def sync(source, destination, ignore: [], delete: false, exclude: [])
|
|
125
|
+
@kdeploy_commands ||= []
|
|
126
|
+
@kdeploy_commands << {
|
|
127
|
+
type: :sync,
|
|
128
|
+
source: source,
|
|
129
|
+
destination: destination,
|
|
130
|
+
ignore: Array(ignore),
|
|
131
|
+
exclude: Array(exclude),
|
|
132
|
+
delete: delete
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
|
|
86
136
|
def inventory(&block)
|
|
87
137
|
instance_eval(&block) if block_given?
|
|
88
138
|
end
|
data/lib/kdeploy/executor.rb
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require 'net/ssh'
|
|
4
4
|
require 'net/scp'
|
|
5
5
|
require 'pathname'
|
|
6
|
+
require 'find'
|
|
7
|
+
require 'shellwords'
|
|
8
|
+
require_relative 'file_filter'
|
|
6
9
|
|
|
7
10
|
module Kdeploy
|
|
8
11
|
# SSH/SCP executor for remote command execution and file operations
|
|
@@ -91,6 +94,51 @@ module Kdeploy
|
|
|
91
94
|
raise TemplateError.new("Template upload failed: #{e.message}", e)
|
|
92
95
|
end
|
|
93
96
|
|
|
97
|
+
def sync_directory(source, destination, ignore: [], exclude: [], delete: false, use_sudo: nil)
|
|
98
|
+
use_sudo = @use_sudo if use_sudo.nil?
|
|
99
|
+
|
|
100
|
+
# Resolve relative paths relative to base_dir
|
|
101
|
+
resolved_source = resolve_path(source)
|
|
102
|
+
|
|
103
|
+
# Validate source directory
|
|
104
|
+
raise FileNotFoundError, "Source directory not found: #{resolved_source}" unless File.directory?(resolved_source)
|
|
105
|
+
|
|
106
|
+
# Create file filter
|
|
107
|
+
all_patterns = ignore + exclude
|
|
108
|
+
filter = FileFilter.new(ignore_patterns: all_patterns)
|
|
109
|
+
|
|
110
|
+
# Collect files to sync
|
|
111
|
+
files_to_sync = collect_files_to_sync(resolved_source, filter)
|
|
112
|
+
|
|
113
|
+
# Upload files
|
|
114
|
+
uploaded_count = 0
|
|
115
|
+
source_path = Pathname.new(resolved_source)
|
|
116
|
+
files_to_sync.each do |file_path|
|
|
117
|
+
relative_path = Pathname.new(file_path).relative_path_from(source_path).to_s
|
|
118
|
+
remote_path = File.join(destination, relative_path).gsub(%r{/+}, '/')
|
|
119
|
+
|
|
120
|
+
# Ensure remote directory exists
|
|
121
|
+
remote_dir = File.dirname(remote_path)
|
|
122
|
+
ensure_remote_directory(remote_dir, use_sudo: use_sudo)
|
|
123
|
+
|
|
124
|
+
# Upload file
|
|
125
|
+
upload(file_path, remote_path, use_sudo: use_sudo)
|
|
126
|
+
uploaded_count += 1
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Delete extra files if requested
|
|
130
|
+
deleted_count = 0
|
|
131
|
+
deleted_count = delete_extra_files(resolved_source, destination, filter, use_sudo: use_sudo) if delete
|
|
132
|
+
|
|
133
|
+
{
|
|
134
|
+
uploaded: uploaded_count,
|
|
135
|
+
deleted: deleted_count,
|
|
136
|
+
total: files_to_sync.size
|
|
137
|
+
}
|
|
138
|
+
rescue StandardError => e
|
|
139
|
+
raise SCPError.new("Directory sync failed: #{e.message}", e)
|
|
140
|
+
end
|
|
141
|
+
|
|
94
142
|
private
|
|
95
143
|
|
|
96
144
|
def upload_with_sudo(source, destination)
|
|
@@ -184,5 +232,67 @@ module Kdeploy
|
|
|
184
232
|
"sudo #{command}"
|
|
185
233
|
end
|
|
186
234
|
end
|
|
235
|
+
|
|
236
|
+
def collect_files_to_sync(source_dir, filter)
|
|
237
|
+
files = []
|
|
238
|
+
source_path = Pathname.new(source_dir)
|
|
239
|
+
|
|
240
|
+
Find.find(source_dir) do |file_path|
|
|
241
|
+
next if File.directory?(file_path)
|
|
242
|
+
|
|
243
|
+
relative_path = Pathname.new(file_path).relative_path_from(source_path).to_s
|
|
244
|
+
next if filter.ignored?(relative_path, source_dir)
|
|
245
|
+
|
|
246
|
+
files << file_path
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
files
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def ensure_remote_directory(remote_dir, use_sudo: nil)
|
|
253
|
+
use_sudo = @use_sudo if use_sudo.nil?
|
|
254
|
+
return if remote_dir.nil? || remote_dir.empty? || remote_dir == '.' || remote_dir == '/'
|
|
255
|
+
|
|
256
|
+
# Create directory with -p flag to create parent directories
|
|
257
|
+
mkdir_command = "mkdir -p #{remote_dir.shellescape}"
|
|
258
|
+
execute(mkdir_command, use_sudo: use_sudo)
|
|
259
|
+
rescue StandardError => e
|
|
260
|
+
# Ignore errors if directory already exists
|
|
261
|
+
error_msg = e.message.downcase
|
|
262
|
+
unless error_msg.include?('exists') || error_msg.include?('file exists') || error_msg.include?('already exists')
|
|
263
|
+
raise
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def delete_extra_files(source_dir, destination_dir, filter, use_sudo: nil)
|
|
268
|
+
use_sudo = @use_sudo if use_sudo.nil?
|
|
269
|
+
|
|
270
|
+
# Get list of remote files
|
|
271
|
+
list_command = "find #{destination_dir.shellescape} -type f 2>/dev/null || true"
|
|
272
|
+
result = execute(list_command, use_sudo: use_sudo)
|
|
273
|
+
remote_files = result[:stdout].lines.map(&:strip).reject(&:empty?)
|
|
274
|
+
|
|
275
|
+
# Get list of local files (relative paths)
|
|
276
|
+
source_path = Pathname.new(source_dir)
|
|
277
|
+
local_files = collect_files_to_sync(source_dir, filter).map do |file_path|
|
|
278
|
+
relative_path = Pathname.new(file_path).relative_path_from(source_path).to_s
|
|
279
|
+
File.join(destination_dir, relative_path).gsub(%r{/+}, '/')
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Find files to delete
|
|
283
|
+
files_to_delete = remote_files - local_files
|
|
284
|
+
|
|
285
|
+
# Delete extra files
|
|
286
|
+
deleted_count = 0
|
|
287
|
+
files_to_delete.each do |file_path|
|
|
288
|
+
delete_command = "rm -f #{file_path.shellescape}"
|
|
289
|
+
execute(delete_command, use_sudo: use_sudo)
|
|
290
|
+
deleted_count += 1
|
|
291
|
+
rescue StandardError
|
|
292
|
+
# Ignore deletion errors
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
deleted_count
|
|
296
|
+
end
|
|
187
297
|
end
|
|
188
298
|
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
5
|
+
module Kdeploy
|
|
6
|
+
# File filter for directory synchronization
|
|
7
|
+
# Supports .gitignore-style patterns
|
|
8
|
+
class FileFilter
|
|
9
|
+
def initialize(ignore_patterns: [])
|
|
10
|
+
@ignore_patterns = normalize_patterns(ignore_patterns)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Check if a file should be ignored
|
|
14
|
+
def ignored?(file_path, base_path = nil)
|
|
15
|
+
relative_path = relative_path_for(file_path, base_path)
|
|
16
|
+
@ignore_patterns.any? { |pattern| match_pattern?(pattern, relative_path) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Filter files from a directory
|
|
20
|
+
def filter_files(files, base_path = nil)
|
|
21
|
+
files.reject { |file| ignored?(file, base_path) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def normalize_patterns(patterns)
|
|
27
|
+
patterns.map do |pattern|
|
|
28
|
+
normalize_pattern(pattern)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def normalize_pattern(pattern)
|
|
33
|
+
# Remove leading slash if present (patterns are relative to base)
|
|
34
|
+
pattern = pattern.sub(%r{\A/}, '')
|
|
35
|
+
# Convert to regex
|
|
36
|
+
pattern_to_regex(pattern)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def pattern_to_regex(pattern)
|
|
40
|
+
# Convert .gitignore-style pattern to regex
|
|
41
|
+
regex_str = pattern
|
|
42
|
+
.gsub('.', '\.') # Escape dots
|
|
43
|
+
.gsub('**', '__STAR_STAR__') # Temporarily replace **
|
|
44
|
+
.gsub('*', '[^/]*') # * matches anything except /
|
|
45
|
+
.gsub('__STAR_STAR__', '.*') # ** matches anything including /
|
|
46
|
+
.gsub('?', '[^/]') # ? matches single char except /
|
|
47
|
+
.gsub('[!', '[^') # [^...] negation
|
|
48
|
+
.gsub('[', '[') # Character class
|
|
49
|
+
|
|
50
|
+
# Anchor to start if pattern doesn't start with **
|
|
51
|
+
regex_str = "^#{regex_str}" unless pattern.start_with?('**')
|
|
52
|
+
# Match end of string or directory separator
|
|
53
|
+
regex_str = "#{regex_str}(/|$)" unless pattern.end_with?('*') || pattern.end_with?('**')
|
|
54
|
+
|
|
55
|
+
Regexp.new(regex_str)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def match_pattern?(pattern, file_path)
|
|
59
|
+
return false if file_path.nil? || file_path.empty?
|
|
60
|
+
|
|
61
|
+
pattern.match?(file_path)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def relative_path_for(file_path, base_path)
|
|
65
|
+
return file_path.to_s unless base_path
|
|
66
|
+
|
|
67
|
+
base = Pathname.new(base_path)
|
|
68
|
+
file = Pathname.new(file_path)
|
|
69
|
+
file.relative_path_from(base).to_s
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
data/lib/kdeploy/initializer.rb
CHANGED
|
@@ -88,6 +88,18 @@ module Kdeploy
|
|
|
88
88
|
sudo apt-get update && sudo apt-get upgrade -y
|
|
89
89
|
SHELL
|
|
90
90
|
end
|
|
91
|
+
|
|
92
|
+
# Example: Directory synchronization task
|
|
93
|
+
task :sync_app, roles: :web do
|
|
94
|
+
# Sync application directory, ignoring development files
|
|
95
|
+
sync './app', '/var/www/app',
|
|
96
|
+
ignore: ['.git', '*.log', 'node_modules', '.env.local', '*.tmp'],
|
|
97
|
+
delete: true
|
|
98
|
+
|
|
99
|
+
# Sync configuration files
|
|
100
|
+
sync './config', '/etc/app',
|
|
101
|
+
exclude: ['*.example', '*.bak']
|
|
102
|
+
end
|
|
91
103
|
RUBY
|
|
92
104
|
end
|
|
93
105
|
|
|
@@ -262,6 +274,11 @@ module Kdeploy
|
|
|
262
274
|
```bash
|
|
263
275
|
kdeploy execute deploy.rb update
|
|
264
276
|
```
|
|
277
|
+
|
|
278
|
+
- **sync_app**: Sync application directory to remote servers
|
|
279
|
+
```bash
|
|
280
|
+
kdeploy execute deploy.rb sync_app
|
|
281
|
+
```
|
|
265
282
|
MD
|
|
266
283
|
end
|
|
267
284
|
|
|
@@ -32,6 +32,17 @@ module Kdeploy
|
|
|
32
32
|
format_file_steps(steps, shown, :upload_template, @pastel.yellow(' === Template ==='), 'upload_template: ')
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
def format_sync_steps(steps, shown)
|
|
36
|
+
output = []
|
|
37
|
+
steps.each do |step|
|
|
38
|
+
next if step_already_shown?(step, :sync, shown)
|
|
39
|
+
|
|
40
|
+
mark_step_as_shown(step, :sync, shown)
|
|
41
|
+
output << format_sync_step(step)
|
|
42
|
+
end
|
|
43
|
+
output
|
|
44
|
+
end
|
|
45
|
+
|
|
35
46
|
def format_file_steps(steps, shown, type, _header, prefix)
|
|
36
47
|
output = []
|
|
37
48
|
steps.each do |step|
|
|
@@ -154,6 +165,10 @@ module Kdeploy
|
|
|
154
165
|
"#{@pastel.blue('>')} Upload: #{command[:source]} -> #{command[:destination]}"
|
|
155
166
|
when :upload_template
|
|
156
167
|
"#{@pastel.blue('>')} Template: #{command[:source]} -> #{command[:destination]}"
|
|
168
|
+
when :sync
|
|
169
|
+
ignore_str = command[:ignore]&.any? ? " (ignore: #{command[:ignore].join(', ')})" : ''
|
|
170
|
+
delete_str = command[:delete] ? ' (delete: true)' : ''
|
|
171
|
+
"#{@pastel.blue('>')} Sync: #{command[:source]} -> #{command[:destination]}#{ignore_str}#{delete_str}"
|
|
157
172
|
else
|
|
158
173
|
"#{@pastel.blue('>')} #{command[:type]}: #{command}"
|
|
159
174
|
end
|
|
@@ -234,5 +249,24 @@ module Kdeploy
|
|
|
234
249
|
key = [step[:command], type].hash
|
|
235
250
|
shown[key] = true
|
|
236
251
|
end
|
|
252
|
+
|
|
253
|
+
def format_sync_step(step)
|
|
254
|
+
duration_str = format_duration(step[:duration])
|
|
255
|
+
sync_path = step[:command].sub('sync: ', '')
|
|
256
|
+
# Truncate long paths for cleaner output
|
|
257
|
+
display_path = sync_path.length > 50 ? "...#{sync_path[-47..]}" : sync_path
|
|
258
|
+
|
|
259
|
+
result = step[:result] || {}
|
|
260
|
+
uploaded = result[:uploaded] || 0
|
|
261
|
+
deleted = result[:deleted] || 0
|
|
262
|
+
total = result[:total] || 0
|
|
263
|
+
|
|
264
|
+
stats = []
|
|
265
|
+
stats << @pastel.green("#{uploaded} uploaded") if uploaded.positive?
|
|
266
|
+
stats << @pastel.yellow("#{deleted} deleted") if deleted.positive?
|
|
267
|
+
stats_str = stats.any? ? " (#{stats.join(', ')})" : " (#{total} files)"
|
|
268
|
+
|
|
269
|
+
@pastel.dim(' 📁 ') + @pastel.cyan(display_path) + @pastel.dim(stats_str) + duration_str
|
|
270
|
+
end
|
|
237
271
|
end
|
|
238
272
|
end
|
data/lib/kdeploy/runner.rb
CHANGED
|
@@ -162,6 +162,8 @@ module Kdeploy
|
|
|
162
162
|
command_executor.execute_upload(command, host_name)
|
|
163
163
|
when :upload_template
|
|
164
164
|
command_executor.execute_upload_template(command, host_name)
|
|
165
|
+
when :sync
|
|
166
|
+
command_executor.execute_sync(command, host_name)
|
|
165
167
|
else
|
|
166
168
|
raise ConfigurationError, "Unknown command type: #{command[:type]}"
|
|
167
169
|
end
|
data/lib/kdeploy/version.rb
CHANGED
data/lib/kdeploy.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative 'kdeploy/errors'
|
|
|
5
5
|
require_relative 'kdeploy/configuration'
|
|
6
6
|
require_relative 'kdeploy/output'
|
|
7
7
|
require_relative 'kdeploy/banner'
|
|
8
|
+
require_relative 'kdeploy/file_filter'
|
|
8
9
|
require_relative 'kdeploy/dsl'
|
|
9
10
|
require_relative 'kdeploy/executor'
|
|
10
11
|
require_relative 'kdeploy/command_grouper'
|
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.28
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kk
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-11-
|
|
11
|
+
date: 2025-11-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bcrypt_pbkdf
|
|
@@ -175,6 +175,7 @@ files:
|
|
|
175
175
|
- lib/kdeploy/dsl.rb
|
|
176
176
|
- lib/kdeploy/errors.rb
|
|
177
177
|
- lib/kdeploy/executor.rb
|
|
178
|
+
- lib/kdeploy/file_filter.rb
|
|
178
179
|
- lib/kdeploy/help_formatter.rb
|
|
179
180
|
- lib/kdeploy/initializer.rb
|
|
180
181
|
- lib/kdeploy/output.rb
|