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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7dba2b99b2c7cca5260a12fbcc3997a94d87daf3f2bc0f1f35c00e4c0aee34cd
4
- data.tar.gz: f5353f6f79ef58a832138343f15861a2e87e6f673d982f8cdf5382746605f55a
3
+ metadata.gz: 115799c766998efdc0b5dba97ed4f3135fe77a2d30b67248db1a52f28915b126
4
+ data.tar.gz: 6eb18a8ddc93efffd1de4da94270766f50883bb2bb2cee91cf50c02cb624ca9d
5
5
  SHA512:
6
- metadata.gz: 447acd705e5cf75e4fcd90c3858b81e00d574679bcc852e669d45969641c247e1d0c585f0ba003d54858154a562faec9aa79686ede914a8488be8f469e3db0ee
7
- data.tar.gz: 3788ade587968dfe3d72499f19c566cfddb70e736d4ef53d25e6c65ea9019f3b0bd065d15d32f3e6a65a9470c72b262faa384a34cb8f091862438b8904d7624a
6
+ metadata.gz: e03e8f2f53ee3349355b4f14ae6ed238b2067350ff1159a46127738a59a5e08afcdcef2635d46536d9441cb001d4823ff074d1e39a03c291581f664c7b56eb05
7
+ data.tar.gz: 67d2d63759fc54b8e68216f3149ca5f936111f9fad72acec45423d396027568778e1c4817240d4c0613869ed8832306f2bbff8fa2cfc7359f357920dac0a6ef5
@@ -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
@@ -1,7 +1,9 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rspec/core/rake_task"
3
+ require "cucumber/rake/task"
3
4
 
4
5
  RSpec::Core::RakeTask.new(:spec)
6
+ Cucumber::Rake::Task.new(:cucumber)
5
7
 
6
- task :default => :spec
8
+ task :default => [:spec, :cucumber]
7
9
 
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
@@ -1,47 +1,13 @@
1
1
  $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
2
- require 'ruby-debug'
3
- require 'grit'
4
- require 'spec/expectations'
5
- gem 'sqlite3-ruby'
2
+ require 'rspec/expectations'
3
+ require 'fileutils'
6
4
 
7
- ENV["PATH"] += ":#{File.dirname(File.expand_path(__FILE__))}/../../bin"
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
- # setup fixtures
15
- if File.exist?("/dev/shm")
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 ssh_server.env
69
- cmd = "#{ssh_server.env} #{command}"
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
- ssh_key = ssh_server.ssh_key ? "-i #{ssh_server.ssh_key} " : ""
77
- ssh_uri = ssh_server.respond_to?(:ssh_uri) ? ssh_server.ssh_uri : ssh_server.ssh_uri(:ssh)
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
- cmd = "ssh -tt #{ssh_key} #{ssh_uri} '#{cmd}'"
80
-
81
- if ssh_server.gateway
82
- gateway_uri = ssh_server.respond_to?(:ssh_uri) ? ssh_server.gateway : ssh_server.ssh_uri(:gateway)
83
- cmd = "ssh -tt #{gateway_uri} \"#{cmd}\""
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, :path
9
- attr_accessor :server, :gateway, :ssh_key, :env
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: server, home: home, verbose: verbose, quiet: quiet)
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: server, home: home, verbose: verbose, quiet: quiet)
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: server, home: home)
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
- uri = URI("scp://#{ssh_uri}")
209
- uri.path = "/#{path}"
210
- uri.path += "/#{file_path}" if file_path
211
- uri
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
@@ -1,4 +1,4 @@
1
1
  module Bard
2
- VERSION = "1.8.0.beta"
2
+ VERSION = "1.8.0.beta2"
3
3
  end
4
4
 
@@ -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
 
@@ -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 user@example.com 'cd /path/to && ls -l'").and_return(["output", "", 0])
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
@@ -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 user@example.com:/path/to/file path/to/file", verbose: false)
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 path/to/file user@example.com:/path/to/file", verbose: false)
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