bard 1.8.0.beta → 1.8.0.beta2
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/.github/workflows/ci.yml +5 -0
- data/Rakefile +3 -1
- data/cucumber.yml +1 -0
- data/features/data.feature +12 -0
- data/features/deploy.feature +13 -0
- data/features/run.feature +13 -0
- data/features/step_definitions/bard_steps.rb +39 -0
- data/features/support/env.rb +5 -39
- data/features/support/test_server.rb +215 -0
- data/lib/bard/command.rb +29 -10
- data/lib/bard/copy.rb +8 -2
- data/lib/bard/target.rb +55 -12
- data/lib/bard/version.rb +1 -1
- data/spec/acceptance/docker/Dockerfile +2 -1
- data/spec/bard/command_spec.rb +1 -1
- data/spec/bard/copy_spec.rb +3 -3
- data/spec/bard/deprecation_spec.rb +79 -0
- data/spec/bard/target_spec.rb +4 -4
- metadata +13 -30
- data/features/bard_check.feature +0 -94
- data/features/bard_deploy.feature +0 -18
- data/features/bard_pull.feature +0 -112
- data/features/bard_push.feature +0 -112
- data/features/podman_testcontainers.feature +0 -16
- data/features/step_definitions/check_steps.rb +0 -47
- data/features/step_definitions/git_steps.rb +0 -73
- data/features/step_definitions/global_steps.rb +0 -56
- data/features/step_definitions/podman_steps.rb +0 -23
- data/features/step_definitions/rails_steps.rb +0 -44
- data/features/step_definitions/submodule_steps.rb +0 -110
- data/features/support/grit_ext.rb +0 -13
- data/features/support/io.rb +0 -32
- data/features/support/podman.rb +0 -153
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 115799c766998efdc0b5dba97ed4f3135fe77a2d30b67248db1a52f28915b126
|
|
4
|
+
data.tar.gz: 6eb18a8ddc93efffd1de4da94270766f50883bb2bb2cee91cf50c02cb624ca9d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e03e8f2f53ee3349355b4f14ae6ed238b2067350ff1159a46127738a59a5e08afcdcef2635d46536d9441cb001d4823ff074d1e39a03c291581f664c7b56eb05
|
|
7
|
+
data.tar.gz: 67d2d63759fc54b8e68216f3149ca5f936111f9fad72acec45423d396027568778e1c4817240d4c0613869ed8832306f2bbff8fa2cfc7359f357920dac0a6ef5
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -34,5 +34,10 @@ jobs:
|
|
|
34
34
|
done
|
|
35
35
|
echo "DOCKER_HOST=tcp://127.0.0.1:8080" >> $GITHUB_ENV
|
|
36
36
|
|
|
37
|
+
- name: Build test container image
|
|
38
|
+
run: |
|
|
39
|
+
sudo podman pull ubuntu:22.04
|
|
40
|
+
sudo podman build -t bard-test-server -f spec/acceptance/docker/Dockerfile spec/acceptance/docker
|
|
41
|
+
|
|
37
42
|
- name: Run tests
|
|
38
43
|
run: bundle exec rake
|
data/Rakefile
CHANGED
data/cucumber.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
default: --publish-quiet
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Feature: bard data
|
|
2
|
+
Copy database from a remote server to local.
|
|
3
|
+
|
|
4
|
+
Background:
|
|
5
|
+
Given a test server is running
|
|
6
|
+
|
|
7
|
+
Scenario: copies database from production to local
|
|
8
|
+
When I run: bard data
|
|
9
|
+
Then the output should contain "Dumping production database to file"
|
|
10
|
+
And the output should contain "Transfering file from production to local"
|
|
11
|
+
And the output should contain "Loading file into local database"
|
|
12
|
+
And a file "db/data.sql.gz" should exist locally
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Feature: bard deploy
|
|
2
|
+
Deploy code changes to a remote server.
|
|
3
|
+
|
|
4
|
+
Background:
|
|
5
|
+
Given a test server is running
|
|
6
|
+
|
|
7
|
+
Scenario: deploys code changes to the remote server
|
|
8
|
+
Given I create a file "DEPLOYED.txt" with content "deployed by bard"
|
|
9
|
+
And I commit the changes with message "Add deployed marker"
|
|
10
|
+
When I run: bard deploy --skip-ci
|
|
11
|
+
Then the output should contain "Deploy Succeeded"
|
|
12
|
+
When I run: bard run "cat DEPLOYED.txt"
|
|
13
|
+
Then the output should contain "deployed by bard"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Feature: bard run
|
|
2
|
+
Execute commands on a remote server.
|
|
3
|
+
|
|
4
|
+
Background:
|
|
5
|
+
Given a test server is running
|
|
6
|
+
|
|
7
|
+
Scenario: executes a command on the remote server
|
|
8
|
+
When I run: bard run "echo hello"
|
|
9
|
+
Then the output should contain "hello"
|
|
10
|
+
|
|
11
|
+
Scenario: operates in the configured path
|
|
12
|
+
When I run: bard run "pwd"
|
|
13
|
+
Then the output should contain "testproject"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Given /^a test server is running$/ do
|
|
2
|
+
raise "Test server failed to start" unless @container && @ssh_port
|
|
3
|
+
end
|
|
4
|
+
|
|
5
|
+
When /^I run: bard (.+)$/ do |command|
|
|
6
|
+
run_bard(command)
|
|
7
|
+
unless @status.success?
|
|
8
|
+
raise "Command failed with status: #{@status}\nOutput: #{@stdout}"
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
When /^I run expecting failure: bard (.+)$/ do |command|
|
|
13
|
+
run_bard(command)
|
|
14
|
+
unless !@status.success?
|
|
15
|
+
raise "Command succeeded but was expected to fail\nOutput: #{@stdout}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Then /^the output should contain "([^\"]+)"$/ do |expected|
|
|
20
|
+
expect(@stdout).to include(expected)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
Given /^I create a file "([^\"]+)" with content "([^\"]+)"$/ do |filename, content|
|
|
24
|
+
Dir.chdir(@test_dir) do
|
|
25
|
+
File.write(filename, content)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
Given /^I commit the changes with message "([^\"]+)"$/ do |message|
|
|
30
|
+
Dir.chdir(@test_dir) do
|
|
31
|
+
system("git add -A", out: File::NULL, err: File::NULL)
|
|
32
|
+
system("git commit -m '#{message}'", out: File::NULL, err: File::NULL)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Then /^a file "([^\"]+)" should exist locally$/ do |filename|
|
|
37
|
+
path = File.join(@test_dir, filename)
|
|
38
|
+
expect(File.exist?(path)).to be(true), "Expected file #{filename} to exist at #{path}"
|
|
39
|
+
end
|
data/features/support/env.rb
CHANGED
|
@@ -1,47 +1,13 @@
|
|
|
1
1
|
$LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
|
|
2
|
-
require '
|
|
3
|
-
require '
|
|
4
|
-
require 'spec/expectations'
|
|
5
|
-
gem 'sqlite3-ruby'
|
|
2
|
+
require 'rspec/expectations'
|
|
3
|
+
require 'fileutils'
|
|
6
4
|
|
|
7
|
-
ENV["PATH"]
|
|
5
|
+
ENV["PATH"] = "#{File.dirname(File.expand_path(__FILE__))}/../../bin:#{ENV['PATH']}"
|
|
8
6
|
ENV["GIT_DIR"] = nil
|
|
9
7
|
ENV["GIT_WORK_TREE"] = nil
|
|
10
8
|
ENV["GIT_INDEX_FILE"] = nil
|
|
11
9
|
|
|
12
10
|
ROOT = File.expand_path(File.dirname(__FILE__) + '/../..')
|
|
13
11
|
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
FileUtils.rm_rf "tmp"
|
|
17
|
-
tmp_dir = "/dev/shm/bard_testing_tmp"
|
|
18
|
-
FileUtils.rm_rf tmp_dir
|
|
19
|
-
FileUtils.mkdir tmp_dir
|
|
20
|
-
`ln -s #{tmp_dir} tmp`
|
|
21
|
-
else
|
|
22
|
-
FileUtils.rm_rf "tmp"
|
|
23
|
-
FileUtils.mkdir "tmp"
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
Dir.chdir 'tmp' do
|
|
27
|
-
`git clone --mirror --recursive #{ROOT}/fixtures/repo origin.git`
|
|
28
|
-
|
|
29
|
-
`git clone --bare --recursive origin.git submodule_a.git`
|
|
30
|
-
`git clone --bare --recursive origin.git submodule_b.git`
|
|
31
|
-
%w(development_a development_b staging production).each do |env|
|
|
32
|
-
`git clone --recursive origin.git #{env}`
|
|
33
|
-
Dir.chdir env do
|
|
34
|
-
FileUtils.cp "config/database.sample.yml", "config/database.yml"
|
|
35
|
-
`grb track master`
|
|
36
|
-
`git checkout master`
|
|
37
|
-
unless env == "production"
|
|
38
|
-
`grb track integration`
|
|
39
|
-
`git checkout integration`
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
FileUtils.mkdir "fixtures"
|
|
44
|
-
Dir.foreach "." do |file|
|
|
45
|
-
FileUtils.mv(file, "fixtures/") unless %w(fixtures . ..).include? file
|
|
46
|
-
end
|
|
47
|
-
end
|
|
12
|
+
# Ensure tmp directory exists
|
|
13
|
+
FileUtils.mkdir_p(File.join(ROOT, "tmp"))
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "open3"
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require "docker-api"
|
|
5
|
+
|
|
6
|
+
module TestServerWorld
|
|
7
|
+
class << self
|
|
8
|
+
attr_accessor :server_available, :image_built
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class PrerequisiteError < StandardError; end
|
|
12
|
+
|
|
13
|
+
def ensure_server_available
|
|
14
|
+
return if TestServerWorld.server_available
|
|
15
|
+
|
|
16
|
+
unless system("command -v podman >/dev/null 2>&1")
|
|
17
|
+
raise PrerequisiteError, "podman is not installed"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
configure_container_socket
|
|
21
|
+
build_test_image
|
|
22
|
+
FileUtils.chmod(0o600, ssh_key_path)
|
|
23
|
+
|
|
24
|
+
TestServerWorld.server_available = true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def configure_container_socket
|
|
28
|
+
if ENV["DOCKER_HOST"]
|
|
29
|
+
Docker.url = ENV["DOCKER_HOST"]
|
|
30
|
+
return
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
socket_path = "/run/user/#{Process.uid}/podman/podman.sock"
|
|
34
|
+
unless File.exist?(socket_path)
|
|
35
|
+
system("systemctl --user start podman.socket 2>/dev/null")
|
|
36
|
+
sleep 2
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
unless File.exist?(socket_path)
|
|
40
|
+
raise PrerequisiteError, "Podman socket not available"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
ENV["DOCKER_HOST"] = "unix://#{socket_path}"
|
|
44
|
+
Docker.url = ENV["DOCKER_HOST"]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def build_test_image
|
|
48
|
+
return if TestServerWorld.image_built
|
|
49
|
+
|
|
50
|
+
# Check if image already exists (e.g., pre-built in CI)
|
|
51
|
+
if image_exists?("bard-test-server")
|
|
52
|
+
TestServerWorld.image_built = true
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
system("podman pull ubuntu:22.04 >/dev/null 2>&1")
|
|
57
|
+
|
|
58
|
+
docker_dir = File.join(ROOT, "spec/acceptance/docker")
|
|
59
|
+
unless system("podman build -t bard-test-server -f #{docker_dir}/Dockerfile #{docker_dir} 2>&1")
|
|
60
|
+
raise PrerequisiteError, "Failed to build test image"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
TestServerWorld.image_built = true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def image_exists?(name)
|
|
67
|
+
Docker::Image.get(name)
|
|
68
|
+
true
|
|
69
|
+
rescue Docker::Error::NotFoundError
|
|
70
|
+
false
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def start_test_server
|
|
74
|
+
ensure_server_available
|
|
75
|
+
|
|
76
|
+
@container = Docker::Container.create(
|
|
77
|
+
"Image" => "localhost/bard-test-server:latest",
|
|
78
|
+
"ExposedPorts" => { "22/tcp" => {} },
|
|
79
|
+
"HostConfig" => {
|
|
80
|
+
"PortBindings" => { "22/tcp" => [{ "HostPort" => "" }] },
|
|
81
|
+
"PublishAllPorts" => true
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
@container.start
|
|
85
|
+
@container.refresh!
|
|
86
|
+
|
|
87
|
+
@ssh_port = @container.info["NetworkSettings"]["Ports"]["22/tcp"].first["HostPort"].to_i
|
|
88
|
+
|
|
89
|
+
wait_for_ssh
|
|
90
|
+
setup_test_directory
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def wait_for_ssh
|
|
94
|
+
30.times do
|
|
95
|
+
return if system(
|
|
96
|
+
"ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null",
|
|
97
|
+
"-o", "ConnectTimeout=1", "-p", @ssh_port.to_s, "-i", ssh_key_path,
|
|
98
|
+
"deploy@localhost", "true",
|
|
99
|
+
out: File::NULL, err: File::NULL
|
|
100
|
+
)
|
|
101
|
+
sleep 0.5
|
|
102
|
+
end
|
|
103
|
+
raise PrerequisiteError, "SSH not ready"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def setup_test_directory
|
|
107
|
+
# Set up git repos on the remote container
|
|
108
|
+
run_ssh "git config --global user.email 'test@example.com'"
|
|
109
|
+
run_ssh "git config --global user.name 'Test User'"
|
|
110
|
+
run_ssh "git config --global init.defaultBranch master"
|
|
111
|
+
run_ssh "mkdir -p ~/repos/testproject.git"
|
|
112
|
+
run_ssh "cd ~/repos/testproject.git && git init --bare"
|
|
113
|
+
run_ssh "git clone ~/repos/testproject.git ~/testproject"
|
|
114
|
+
run_ssh "mkdir -p ~/testproject/bin ~/testproject/db"
|
|
115
|
+
|
|
116
|
+
# bin/setup script
|
|
117
|
+
run_ssh "echo '#!/bin/bash' > ~/testproject/bin/setup"
|
|
118
|
+
run_ssh "echo 'echo Setup complete' >> ~/testproject/bin/setup"
|
|
119
|
+
run_ssh "chmod +x ~/testproject/bin/setup"
|
|
120
|
+
|
|
121
|
+
# bin/rake script for db:dump and db:load
|
|
122
|
+
run_ssh <<~'SETUP'
|
|
123
|
+
cat > ~/testproject/bin/rake << 'SCRIPT'
|
|
124
|
+
#!/bin/bash
|
|
125
|
+
case "$1" in
|
|
126
|
+
db:dump)
|
|
127
|
+
echo "production data" | gzip > db/data.sql.gz
|
|
128
|
+
;;
|
|
129
|
+
db:load)
|
|
130
|
+
gunzip -c db/data.sql.gz > /dev/null
|
|
131
|
+
echo "Data loaded"
|
|
132
|
+
;;
|
|
133
|
+
esac
|
|
134
|
+
SCRIPT
|
|
135
|
+
SETUP
|
|
136
|
+
run_ssh "chmod +x ~/testproject/bin/rake"
|
|
137
|
+
run_ssh "cd ~/testproject && git add . && git commit -m 'Initial commit'"
|
|
138
|
+
run_ssh "cd ~/testproject && git push origin master"
|
|
139
|
+
|
|
140
|
+
# Set up local git repo in isolated temp directory
|
|
141
|
+
setup_local_git_repo
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def setup_local_git_repo
|
|
145
|
+
@test_dir = Dir.mktmpdir("bard_test")
|
|
146
|
+
@ssh_command = "ssh -i #{ssh_key_path} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
|
147
|
+
|
|
148
|
+
Dir.chdir(@test_dir) do
|
|
149
|
+
# Clone directly into the temp directory (pass SSH command via env, not global ENV)
|
|
150
|
+
ssh_url = "ssh://deploy@localhost:#{@ssh_port}/home/deploy/repos/testproject.git"
|
|
151
|
+
system({ "GIT_SSH_COMMAND" => @ssh_command }, "git clone #{ssh_url} .", out: File::NULL, err: File::NULL)
|
|
152
|
+
|
|
153
|
+
# Configure git settings locally in this repo only
|
|
154
|
+
system("git config user.email 'test@example.com'", out: File::NULL, err: File::NULL)
|
|
155
|
+
system("git config user.name 'Test User'", out: File::NULL, err: File::NULL)
|
|
156
|
+
system("git config core.sshCommand '#{@ssh_command}'", out: File::NULL, err: File::NULL)
|
|
157
|
+
|
|
158
|
+
# Ensure db directory exists locally
|
|
159
|
+
FileUtils.mkdir_p("db")
|
|
160
|
+
|
|
161
|
+
# Write bard config in the test directory
|
|
162
|
+
File.write("bard.rb", <<~RUBY)
|
|
163
|
+
target :production do
|
|
164
|
+
ssh "deploy@localhost:#{@ssh_port}",
|
|
165
|
+
path: "testproject",
|
|
166
|
+
ssh_key: "#{ssh_key_path}"
|
|
167
|
+
ping false
|
|
168
|
+
end
|
|
169
|
+
RUBY
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def run_ssh(command)
|
|
174
|
+
stdout, status = Open3.capture2e(
|
|
175
|
+
"ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null",
|
|
176
|
+
"-p", @ssh_port.to_s, "-i", ssh_key_path,
|
|
177
|
+
"deploy@localhost", command
|
|
178
|
+
)
|
|
179
|
+
unless status.success?
|
|
180
|
+
raise PrerequisiteError, "SSH command failed: #{command}\nOutput: #{stdout}"
|
|
181
|
+
end
|
|
182
|
+
true
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def run_bard(command)
|
|
186
|
+
Dir.chdir(@test_dir) do
|
|
187
|
+
@stdout, @status = Open3.capture2e("bard #{command}")
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def ssh_key_path
|
|
192
|
+
File.join(ROOT, "spec/acceptance/docker/test_key")
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def stop_test_server
|
|
196
|
+
return unless @container
|
|
197
|
+
@container.stop rescue nil
|
|
198
|
+
@container.delete(force: true) rescue nil
|
|
199
|
+
ensure
|
|
200
|
+
@container = nil
|
|
201
|
+
@ssh_port = nil
|
|
202
|
+
FileUtils.rm_rf(@test_dir) if @test_dir
|
|
203
|
+
@test_dir = nil
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
World(TestServerWorld)
|
|
208
|
+
|
|
209
|
+
Before do
|
|
210
|
+
start_test_server
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
After do
|
|
214
|
+
stop_test_server
|
|
215
|
+
end
|
data/lib/bard/command.rb
CHANGED
|
@@ -64,25 +64,44 @@ module Bard
|
|
|
64
64
|
# Support both new Target (with server attribute) and old Server architecture
|
|
65
65
|
ssh_server = on.respond_to?(:server) ? on.server : on
|
|
66
66
|
|
|
67
|
+
# Get options from Target first (for deprecated separate method calls), fall back to SSHServer
|
|
68
|
+
env_value = on.respond_to?(:env) ? on.env : nil
|
|
69
|
+
env_value ||= ssh_server.env if ssh_server.respond_to?(:env)
|
|
70
|
+
|
|
71
|
+
ssh_key = on.respond_to?(:ssh_key) ? on.ssh_key : nil
|
|
72
|
+
ssh_key ||= ssh_server.ssh_key if ssh_server.respond_to?(:ssh_key)
|
|
73
|
+
|
|
74
|
+
gateway = on.respond_to?(:gateway) ? on.gateway : nil
|
|
75
|
+
gateway ||= ssh_server.gateway if ssh_server.respond_to?(:gateway)
|
|
76
|
+
|
|
67
77
|
cmd = command
|
|
68
|
-
if
|
|
69
|
-
|
|
70
|
-
end
|
|
78
|
+
cmd = "#{env_value} #{command}" if env_value
|
|
79
|
+
|
|
71
80
|
unless home
|
|
72
81
|
path = on.respond_to?(:path) ? on.path : ssh_server.path
|
|
73
82
|
cmd = "cd #{path} && #{cmd}" if path
|
|
74
83
|
end
|
|
75
84
|
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
ssh_opts = ["-tt", "-o StrictHostKeyChecking=no", "-o UserKnownHostsFile=/dev/null", "-o LogLevel=ERROR"]
|
|
86
|
+
ssh_opts << "-i #{ssh_key}" if ssh_key
|
|
78
87
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
# Handle new SSHServer vs old Server architecture
|
|
89
|
+
if ssh_server.respond_to?(:host)
|
|
90
|
+
# New SSHServer - has separate host/port/user
|
|
91
|
+
ssh_opts << "-p #{ssh_server.port}" if ssh_server.port && ssh_server.port != "22"
|
|
92
|
+
ssh_opts << "-o ProxyJump=#{gateway}" if gateway
|
|
93
|
+
ssh_target = "#{ssh_server.user}@#{ssh_server.host}"
|
|
94
|
+
else
|
|
95
|
+
# Old Server - uses URI-based ssh_uri
|
|
96
|
+
ssh_target = ssh_server.ssh_uri
|
|
97
|
+
if gateway
|
|
98
|
+
gateway_uri = ssh_server.ssh_uri(:gateway)
|
|
99
|
+
ssh_opts << "-o ProxyJump=#{gateway_uri}"
|
|
100
|
+
end
|
|
84
101
|
end
|
|
85
102
|
|
|
103
|
+
cmd = "ssh #{ssh_opts.join(' ')} #{ssh_target} '#{cmd}'"
|
|
104
|
+
|
|
86
105
|
cmd += " 2>&1" if quiet
|
|
87
106
|
cmd
|
|
88
107
|
end
|
data/lib/bard/copy.rb
CHANGED
|
@@ -29,10 +29,16 @@ module Bard
|
|
|
29
29
|
|
|
30
30
|
ssh_key = ssh_server.ssh_key ? "-i #{ssh_server.ssh_key}" : ""
|
|
31
31
|
|
|
32
|
+
ssh_opts = "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR"
|
|
33
|
+
|
|
34
|
+
# scp uses -P for port (uppercase, unlike ssh's -p)
|
|
35
|
+
port = ssh_server.port
|
|
36
|
+
port_opt = port && port.to_s != "22" ? "-P #{port}" : ""
|
|
37
|
+
|
|
32
38
|
from_and_to = [path, target_or_server.scp_uri(path)]
|
|
33
39
|
from_and_to.reverse! if direction == :from
|
|
34
40
|
|
|
35
|
-
command = ["scp", gateway, ssh_key, *from_and_to].join(" ")
|
|
41
|
+
command = ["scp", ssh_opts, gateway, ssh_key, port_opt, *from_and_to].reject(&:empty?).join(" ")
|
|
36
42
|
Bard::Command.run! command, verbose: verbose
|
|
37
43
|
end
|
|
38
44
|
|
|
@@ -113,7 +119,7 @@ module Bard
|
|
|
113
119
|
from_str = "-p#{from_uri.port || 22} #{from_uri.user}@#{from_uri.host}"
|
|
114
120
|
to_str = to.rsync_uri(path).sub(%r(/[^/]+$), '/')
|
|
115
121
|
|
|
116
|
-
command = %(ssh -A #{from_str} 'rsync -e \"ssh -A -p#{to_uri.port || 22} -o StrictHostKeyChecking=no\" --delete --info=progress2 -az #{from.path}/#{path} #{to_str}')
|
|
122
|
+
command = %(ssh -A #{from_str} 'rsync -e \"ssh -A -p#{to_uri.port || 22} -o StrictHostKeyChecking=no -o LogLevel=ERROR\" --delete --info=progress2 -az #{from.path}/#{path} #{to_str}')
|
|
117
123
|
Bard::Command.run! command, verbose: verbose
|
|
118
124
|
end
|
|
119
125
|
end
|
data/lib/bard/target.rb
CHANGED
|
@@ -2,11 +2,12 @@ require "uri"
|
|
|
2
2
|
require "bard/command"
|
|
3
3
|
require "bard/copy"
|
|
4
4
|
require "bard/deploy_strategy"
|
|
5
|
+
require "bard/deprecation"
|
|
5
6
|
|
|
6
7
|
module Bard
|
|
7
8
|
class Target
|
|
8
|
-
attr_reader :key, :config
|
|
9
|
-
attr_accessor :server
|
|
9
|
+
attr_reader :key, :config
|
|
10
|
+
attr_accessor :server
|
|
10
11
|
|
|
11
12
|
def initialize(key, config)
|
|
12
13
|
@key = key
|
|
@@ -67,7 +68,7 @@ module Bard
|
|
|
67
68
|
|
|
68
69
|
# Auto-configure ping from hostname
|
|
69
70
|
hostname = @server.hostname
|
|
70
|
-
ping(hostname) if hostname
|
|
71
|
+
ping("https://#{hostname}") if hostname
|
|
71
72
|
end
|
|
72
73
|
end
|
|
73
74
|
|
|
@@ -78,12 +79,41 @@ module Bard
|
|
|
78
79
|
# Path configuration
|
|
79
80
|
def path(new_path = nil)
|
|
80
81
|
if new_path
|
|
82
|
+
Deprecation.warn "Separate `path` call is deprecated; pass as keyword argument to `ssh` instead, e.g., `ssh \"user@host\", path: \"#{new_path}\"` (will be removed in v2.0)"
|
|
81
83
|
@path = new_path
|
|
82
84
|
else
|
|
83
85
|
@path || config.project_name
|
|
84
86
|
end
|
|
85
87
|
end
|
|
86
88
|
|
|
89
|
+
# Deprecated separate setter methods - use ssh(..., option: value) instead
|
|
90
|
+
def gateway(value = nil)
|
|
91
|
+
if value
|
|
92
|
+
Deprecation.warn "Separate `gateway` call is deprecated; pass as keyword argument to `ssh` instead, e.g., `ssh \"user@host\", gateway: \"#{value}\"` (will be removed in v2.0)"
|
|
93
|
+
@gateway = value
|
|
94
|
+
else
|
|
95
|
+
@gateway
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def ssh_key(value = nil)
|
|
100
|
+
if value
|
|
101
|
+
Deprecation.warn "Separate `ssh_key` call is deprecated; pass as keyword argument to `ssh` instead, e.g., `ssh \"user@host\", ssh_key: \"#{value}\"` (will be removed in v2.0)"
|
|
102
|
+
@ssh_key = value
|
|
103
|
+
else
|
|
104
|
+
@ssh_key
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def env(value = nil)
|
|
109
|
+
if value
|
|
110
|
+
Deprecation.warn "Separate `env` call is deprecated; pass as keyword argument to `ssh` instead, e.g., `ssh \"user@host\", env: \"#{value}\"` (will be removed in v2.0)"
|
|
111
|
+
@env = value
|
|
112
|
+
else
|
|
113
|
+
@env
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
87
117
|
# Ping configuration
|
|
88
118
|
def ping(*urls)
|
|
89
119
|
if urls.empty?
|
|
@@ -134,6 +164,18 @@ module Bard
|
|
|
134
164
|
end
|
|
135
165
|
end
|
|
136
166
|
|
|
167
|
+
# Deprecated strategy configuration methods
|
|
168
|
+
def strategy(name)
|
|
169
|
+
Deprecation.warn "`strategy` is deprecated; use the strategy method directly, e.g., `#{name} \"url\"` instead of `strategy :#{name}` (will be removed in v2.0)"
|
|
170
|
+
@deploy_strategy = name
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def option(key, value)
|
|
174
|
+
Deprecation.warn "`option` is deprecated; pass options as keyword arguments to the strategy method, e.g., `jets \"url\", #{key}: #{value.inspect}` (will be removed in v2.0)"
|
|
175
|
+
@strategy_options_hash[@deploy_strategy] ||= {}
|
|
176
|
+
@strategy_options_hash[@deploy_strategy][key] = value
|
|
177
|
+
end
|
|
178
|
+
|
|
137
179
|
def strategy_options(strategy_name)
|
|
138
180
|
@strategy_options_hash[strategy_name] || {}
|
|
139
181
|
end
|
|
@@ -177,38 +219,39 @@ module Bard
|
|
|
177
219
|
# Remote command execution
|
|
178
220
|
def run!(command, home: false, verbose: false, quiet: false)
|
|
179
221
|
require_capability!(:ssh)
|
|
180
|
-
Command.run!(command, on:
|
|
222
|
+
Command.run!(command, on: self, home: home, verbose: verbose, quiet: quiet)
|
|
181
223
|
end
|
|
182
224
|
|
|
183
225
|
def run(command, home: false, verbose: false, quiet: false)
|
|
184
226
|
require_capability!(:ssh)
|
|
185
|
-
Command.run(command, on:
|
|
227
|
+
Command.run(command, on: self, home: home, verbose: verbose, quiet: quiet)
|
|
186
228
|
end
|
|
187
229
|
|
|
188
230
|
def exec!(command, home: false)
|
|
189
231
|
require_capability!(:ssh)
|
|
190
|
-
Command.exec!(command, on:
|
|
232
|
+
Command.exec!(command, on: self, home: home)
|
|
191
233
|
end
|
|
192
234
|
|
|
193
235
|
# File transfer
|
|
194
236
|
def copy_file(path, to:, verbose: false)
|
|
195
237
|
require_capability!(:ssh)
|
|
196
|
-
to.require_capability!(:ssh)
|
|
238
|
+
to.require_capability!(:ssh) if to.respond_to?(:require_capability!)
|
|
197
239
|
Copy.file(path, from: self, to: to, verbose: verbose)
|
|
198
240
|
end
|
|
199
241
|
|
|
200
242
|
def copy_dir(path, to:, verbose: false)
|
|
201
243
|
require_capability!(:ssh)
|
|
202
|
-
to.require_capability!(:ssh)
|
|
244
|
+
to.require_capability!(:ssh) if to.respond_to?(:require_capability!)
|
|
203
245
|
Copy.dir(path, from: self, to: to, verbose: verbose)
|
|
204
246
|
end
|
|
205
247
|
|
|
206
248
|
# URI methods for compatibility
|
|
207
249
|
def scp_uri(file_path = nil)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
250
|
+
# Use traditional scp format: user@host:path (relative to home)
|
|
251
|
+
# Port is NOT included here - it must be passed via -P flag to scp
|
|
252
|
+
full_path = path
|
|
253
|
+
full_path += "/#{file_path}" if file_path
|
|
254
|
+
"#{server.user}@#{server.host}:#{full_path}"
|
|
212
255
|
end
|
|
213
256
|
|
|
214
257
|
def rsync_uri(file_path = nil)
|
data/lib/bard/version.rb
CHANGED
|
@@ -3,10 +3,11 @@ FROM ubuntu:22.04
|
|
|
3
3
|
# Prevent interactive prompts
|
|
4
4
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
5
5
|
|
|
6
|
-
# Install SSH server and basic tools
|
|
6
|
+
# Install SSH server, git, and basic tools
|
|
7
7
|
RUN apt-get update && \
|
|
8
8
|
apt-get install -y \
|
|
9
9
|
openssh-server \
|
|
10
|
+
git \
|
|
10
11
|
sudo \
|
|
11
12
|
&& rm -rf /var/lib/apt/lists/*
|
|
12
13
|
|
data/spec/bard/command_spec.rb
CHANGED
|
@@ -11,7 +11,7 @@ describe Bard::Command do
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
it "should run a command on a remote server" do
|
|
14
|
-
expect(Open3).to receive(:capture3).with("ssh -tt
|
|
14
|
+
expect(Open3).to receive(:capture3).with("ssh -tt -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR user@example.com 'cd /path/to && ls -l'").and_return(["output", "", 0])
|
|
15
15
|
Bard::Command.run "ls -l", on: remote
|
|
16
16
|
end
|
|
17
17
|
end
|
data/spec/bard/copy_spec.rb
CHANGED
|
@@ -2,17 +2,17 @@ require "spec_helper"
|
|
|
2
2
|
require "bard/copy"
|
|
3
3
|
|
|
4
4
|
describe Bard::Copy do
|
|
5
|
-
let(:production) { double("production", key: :production, scp_uri: "user@example.com:/path/to/file", rsync_uri: "user@example.com:/path/to/", gateway: nil, ssh_key: nil, path: "/path/to") }
|
|
5
|
+
let(:production) { double("production", key: :production, scp_uri: "user@example.com:/path/to/file", rsync_uri: "user@example.com:/path/to/", gateway: nil, ssh_key: nil, port: "22", path: "/path/to") }
|
|
6
6
|
let(:local) { double("local", key: :local) }
|
|
7
7
|
|
|
8
8
|
context ".file" do
|
|
9
9
|
it "should copy a file from a remote server to the local machine" do
|
|
10
|
-
expect(Bard::Command).to receive(:run!).with("scp
|
|
10
|
+
expect(Bard::Command).to receive(:run!).with("scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR user@example.com:/path/to/file path/to/file", verbose: false)
|
|
11
11
|
Bard::Copy.file "path/to/file", from: production, to: local
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
it "should copy a file from the local machine to a remote server" do
|
|
15
|
-
expect(Bard::Command).to receive(:run!).with("scp
|
|
15
|
+
expect(Bard::Command).to receive(:run!).with("scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR path/to/file user@example.com:/path/to/file", verbose: false)
|
|
16
16
|
Bard::Copy.file "path/to/file", from: local, to: production
|
|
17
17
|
end
|
|
18
18
|
end
|