bard 1.8.0 → 2.0.0.beta

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +0 -5
  3. data/MIGRATION_GUIDE.md +9 -24
  4. data/Rakefile +1 -3
  5. data/features/bard_check.feature +94 -0
  6. data/features/bard_deploy.feature +18 -0
  7. data/features/bard_pull.feature +112 -0
  8. data/features/bard_push.feature +112 -0
  9. data/features/podman_testcontainers.feature +16 -0
  10. data/features/step_definitions/check_steps.rb +47 -0
  11. data/features/step_definitions/git_steps.rb +73 -0
  12. data/features/step_definitions/global_steps.rb +56 -0
  13. data/features/step_definitions/podman_steps.rb +23 -0
  14. data/features/step_definitions/rails_steps.rb +44 -0
  15. data/features/step_definitions/submodule_steps.rb +110 -0
  16. data/features/support/env.rb +39 -5
  17. data/features/support/grit_ext.rb +13 -0
  18. data/features/support/io.rb +32 -0
  19. data/features/support/podman.rb +153 -0
  20. data/lib/bard/command.rb +10 -29
  21. data/lib/bard/config.rb +0 -2
  22. data/lib/bard/copy.rb +33 -12
  23. data/lib/bard/server.rb +1 -43
  24. data/lib/bard/ssh_server.rb +1 -1
  25. data/lib/bard/target.rb +15 -65
  26. data/lib/bard/version.rb +1 -1
  27. data/spec/acceptance/docker/Dockerfile +1 -2
  28. data/spec/bard/command_spec.rb +1 -1
  29. data/spec/bard/copy_spec.rb +3 -3
  30. data/spec/bard/ssh_server_spec.rb +3 -7
  31. data/spec/bard/target_spec.rb +5 -9
  32. metadata +30 -16
  33. data/cucumber.yml +0 -1
  34. data/features/data.feature +0 -12
  35. data/features/deploy.feature +0 -13
  36. data/features/run.feature +0 -13
  37. data/features/step_definitions/bard_steps.rb +0 -39
  38. data/features/support/test_server.rb +0 -215
  39. data/lib/bard/deprecation.rb +0 -19
  40. data/spec/bard/deprecation_spec.rb +0 -281
data/lib/bard/target.rb CHANGED
@@ -2,12 +2,11 @@ require "uri"
2
2
  require "bard/command"
3
3
  require "bard/copy"
4
4
  require "bard/deploy_strategy"
5
- require "bard/deprecation"
6
5
 
7
6
  module Bard
8
7
  class Target
9
- attr_reader :key, :config
10
- attr_accessor :server
8
+ attr_reader :key, :config, :path
9
+ attr_accessor :server, :gateway, :ssh_key, :env
11
10
 
12
11
  def initialize(key, config)
13
12
  @key = key
@@ -68,7 +67,7 @@ module Bard
68
67
 
69
68
  # Auto-configure ping from hostname
70
69
  hostname = @server.hostname
71
- ping("https://#{hostname}") if hostname
70
+ ping(hostname) if hostname
72
71
  end
73
72
  end
74
73
 
@@ -79,46 +78,17 @@ module Bard
79
78
  # Path configuration
80
79
  def path(new_path = nil)
81
80
  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)"
83
81
  @path = new_path
84
82
  else
85
83
  @path || config.project_name
86
84
  end
87
85
  end
88
86
 
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
-
117
87
  # Ping configuration
118
88
  def ping(*urls)
119
89
  if urls.empty?
120
- # Getter - normalize URLs like Server does
121
- @ping_urls.map { |url| normalize_ping(url) }
90
+ # Getter
91
+ @ping_urls
122
92
  elsif urls.first == false
123
93
  # Disable ping
124
94
  @ping_urls = []
@@ -164,18 +134,6 @@ module Bard
164
134
  end
165
135
  end
166
136
 
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
-
179
137
  def strategy_options(strategy_name)
180
138
  @strategy_options_hash[strategy_name] || {}
181
139
  end
@@ -219,41 +177,42 @@ module Bard
219
177
  # Remote command execution
220
178
  def run!(command, home: false, verbose: false, quiet: false)
221
179
  require_capability!(:ssh)
222
- Command.run!(command, on: self, home: home, verbose: verbose, quiet: quiet)
180
+ Command.run!(command, on: server, home: home, verbose: verbose, quiet: quiet)
223
181
  end
224
182
 
225
183
  def run(command, home: false, verbose: false, quiet: false)
226
184
  require_capability!(:ssh)
227
- Command.run(command, on: self, home: home, verbose: verbose, quiet: quiet)
185
+ Command.run(command, on: server, home: home, verbose: verbose, quiet: quiet)
228
186
  end
229
187
 
230
188
  def exec!(command, home: false)
231
189
  require_capability!(:ssh)
232
- Command.exec!(command, on: self, home: home)
190
+ Command.exec!(command, on: server, home: home)
233
191
  end
234
192
 
235
193
  # File transfer
236
194
  def copy_file(path, to:, verbose: false)
237
195
  require_capability!(:ssh)
238
- to.require_capability!(:ssh) if to.respond_to?(:require_capability!)
196
+ to.require_capability!(:ssh)
239
197
  Copy.file(path, from: self, to: to, verbose: verbose)
240
198
  end
241
199
 
242
200
  def copy_dir(path, to:, verbose: false)
243
201
  require_capability!(:ssh)
244
- to.require_capability!(:ssh) if to.respond_to?(:require_capability!)
202
+ to.require_capability!(:ssh)
245
203
  Copy.dir(path, from: self, to: to, verbose: verbose)
246
204
  end
247
205
 
248
206
  # URI methods for compatibility
249
207
  def scp_uri(file_path = nil)
250
- full_path = "/#{path}"
251
- full_path += "/#{file_path}" if file_path
252
- URI::Generic.build(scheme: "scp", userinfo: server.user, host: server.host, port: server.port.to_i, path: full_path)
208
+ uri = URI("scp://#{ssh_uri}")
209
+ uri.path = "/#{path}"
210
+ uri.path += "/#{file_path}" if file_path
211
+ uri
253
212
  end
254
213
 
255
214
  def rsync_uri(file_path = nil)
256
- uri = ssh_uri
215
+ uri = URI("ssh://#{ssh_uri}")
257
216
  str = "#{uri.user}@#{uri.host}"
258
217
  str += ":#{path}"
259
218
  str += "/#{file_path}" if file_path
@@ -276,14 +235,5 @@ module Bard
276
235
  end
277
236
  end
278
237
  end
279
-
280
- private
281
-
282
- def normalize_ping(value)
283
- return nil if value == false || value.nil?
284
- normalized = value.to_s
285
- normalized = "https://#{normalized}" unless normalized.start_with?("http")
286
- normalized
287
- end
288
238
  end
289
239
  end
data/lib/bard/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Bard
2
- VERSION = "1.8.0"
2
+ VERSION = "2.0.0.beta"
3
3
  end
4
4
 
@@ -3,11 +3,10 @@ FROM ubuntu:22.04
3
3
  # Prevent interactive prompts
4
4
  ENV DEBIAN_FRONTEND=noninteractive
5
5
 
6
- # Install SSH server, git, and basic tools
6
+ # Install SSH server and basic tools
7
7
  RUN apt-get update && \
8
8
  apt-get install -y \
9
9
  openssh-server \
10
- git \
11
10
  sudo \
12
11
  && rm -rf /var/lib/apt/lists/*
13
12
 
@@ -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 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR user@example.com 'cd /path/to && ls -l'").and_return(["output", "", 0])
14
+ expect(Open3).to receive(:capture3).with("ssh -tt 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, port: "22", 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, 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 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR user@example.com:/path/to/file path/to/file", verbose: false)
10
+ expect(Bard::Command).to receive(:run!).with("scp 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 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR path/to/file user@example.com:/path/to/file", verbose: false)
15
+ expect(Bard::Command).to receive(:run!).with("scp 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
@@ -40,18 +40,14 @@ describe Bard::SSHServer do
40
40
  end
41
41
 
42
42
  describe "#ssh_uri" do
43
- it "returns a URI object" do
43
+ it "returns the SSH connection string" do
44
44
  server = described_class.new("deploy@example.com:22")
45
- expect(server.ssh_uri).to be_a(URI::Generic)
46
- expect(server.ssh_uri.scheme).to eq("ssh")
47
- expect(server.ssh_uri.user).to eq("deploy")
48
- expect(server.ssh_uri.host).to eq("example.com")
49
- expect(server.ssh_uri.port).to eq(22)
45
+ expect(server.ssh_uri).to eq("deploy@example.com:22")
50
46
  end
51
47
 
52
48
  it "includes port if non-standard" do
53
49
  server = described_class.new("deploy@example.com:2222")
54
- expect(server.ssh_uri.port).to eq(2222)
50
+ expect(server.ssh_uri).to eq("deploy@example.com:2222")
55
51
  end
56
52
  end
57
53
 
@@ -37,11 +37,7 @@ describe Bard::Target do
37
37
  end
38
38
 
39
39
  it "parses SSH URI" do
40
- expect(target.ssh_uri).to be_a(URI::Generic)
41
- expect(target.ssh_uri.scheme).to eq("ssh")
42
- expect(target.ssh_uri.user).to eq("deploy")
43
- expect(target.ssh_uri.host).to eq("example.com")
44
- expect(target.ssh_uri.port).to eq(22)
40
+ expect(target.ssh_uri).to eq("deploy@example.com:22")
45
41
  end
46
42
  end
47
43
 
@@ -76,7 +72,7 @@ describe Bard::Target do
76
72
  end
77
73
 
78
74
  it "auto-configures ping URL from hostname" do
79
- expect(target.ping_urls).to include("https://example.com")
75
+ expect(target.ping_urls).to include("example.com")
80
76
  end
81
77
  end
82
78
 
@@ -141,7 +137,7 @@ describe Bard::Target do
141
137
 
142
138
  it "executes command on remote server" do
143
139
  expect(Bard::Command).to receive(:run!)
144
- .with("ls", on: target, home: false, verbose: false, quiet: false)
140
+ .with("ls", on: target.server, home: false, verbose: false, quiet: false)
145
141
  target.run!("ls")
146
142
  end
147
143
  end
@@ -155,7 +151,7 @@ describe Bard::Target do
155
151
 
156
152
  it "executes command on remote server without raising" do
157
153
  expect(Bard::Command).to receive(:run)
158
- .with("ls", on: target, home: false, verbose: false, quiet: false)
154
+ .with("ls", on: target.server, home: false, verbose: false, quiet: false)
159
155
  target.run("ls")
160
156
  end
161
157
  end
@@ -169,7 +165,7 @@ describe Bard::Target do
169
165
 
170
166
  it "replaces process with remote command" do
171
167
  expect(Bard::Command).to receive(:exec!)
172
- .with("ls", on: target, home: false)
168
+ .with("ls", on: target.server, home: false)
173
169
  target.exec!("ls")
174
170
  end
175
171
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bard
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.0
4
+ version: 2.0.0.beta
5
5
  platform: ruby
6
6
  authors:
7
7
  - Micah Geisel
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-01-22 00:00:00.000000000 Z
10
+ date: 2025-12-18 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: thor
@@ -171,13 +171,21 @@ files:
171
171
  - bard.gemspec
172
172
  - bin/bard
173
173
  - bin/setup
174
- - cucumber.yml
175
- - features/data.feature
176
- - features/deploy.feature
177
- - features/run.feature
178
- - features/step_definitions/bard_steps.rb
174
+ - features/bard_check.feature
175
+ - features/bard_deploy.feature
176
+ - features/bard_pull.feature
177
+ - features/bard_push.feature
178
+ - features/podman_testcontainers.feature
179
+ - features/step_definitions/check_steps.rb
180
+ - features/step_definitions/git_steps.rb
181
+ - features/step_definitions/global_steps.rb
182
+ - features/step_definitions/podman_steps.rb
183
+ - features/step_definitions/rails_steps.rb
184
+ - features/step_definitions/submodule_steps.rb
179
185
  - features/support/env.rb
180
- - features/support/test_server.rb
186
+ - features/support/grit_ext.rb
187
+ - features/support/io.rb
188
+ - features/support/podman.rb
181
189
  - install_files/.github/dependabot.yml
182
190
  - install_files/.github/workflows/cache-ci.yml
183
191
  - install_files/.github/workflows/ci.yml
@@ -219,7 +227,6 @@ files:
219
227
  - lib/bard/deploy_strategy.rb
220
228
  - lib/bard/deploy_strategy/github_pages.rb
221
229
  - lib/bard/deploy_strategy/ssh.rb
222
- - lib/bard/deprecation.rb
223
230
  - lib/bard/git.rb
224
231
  - lib/bard/github.rb
225
232
  - lib/bard/github_pages.rb
@@ -272,7 +279,6 @@ files:
272
279
  - spec/bard/copy_spec.rb
273
280
  - spec/bard/deploy_strategy/ssh_spec.rb
274
281
  - spec/bard/deploy_strategy_spec.rb
275
- - spec/bard/deprecation_spec.rb
276
282
  - spec/bard/dynamic_dsl_spec.rb
277
283
  - spec/bard/git_spec.rb
278
284
  - spec/bard/github_pages_spec.rb
@@ -323,12 +329,21 @@ rubygems_version: 3.6.2
323
329
  specification_version: 4
324
330
  summary: CLI to automate common development tasks.
325
331
  test_files:
326
- - features/data.feature
327
- - features/deploy.feature
328
- - features/run.feature
329
- - features/step_definitions/bard_steps.rb
332
+ - features/bard_check.feature
333
+ - features/bard_deploy.feature
334
+ - features/bard_pull.feature
335
+ - features/bard_push.feature
336
+ - features/podman_testcontainers.feature
337
+ - features/step_definitions/check_steps.rb
338
+ - features/step_definitions/git_steps.rb
339
+ - features/step_definitions/global_steps.rb
340
+ - features/step_definitions/podman_steps.rb
341
+ - features/step_definitions/rails_steps.rb
342
+ - features/step_definitions/submodule_steps.rb
330
343
  - features/support/env.rb
331
- - features/support/test_server.rb
344
+ - features/support/grit_ext.rb
345
+ - features/support/io.rb
346
+ - features/support/podman.rb
332
347
  - spec/acceptance/.gitignore
333
348
  - spec/acceptance/docker/Dockerfile
334
349
  - spec/acceptance/docker/test_key
@@ -357,7 +372,6 @@ test_files:
357
372
  - spec/bard/copy_spec.rb
358
373
  - spec/bard/deploy_strategy/ssh_spec.rb
359
374
  - spec/bard/deploy_strategy_spec.rb
360
- - spec/bard/deprecation_spec.rb
361
375
  - spec/bard/dynamic_dsl_spec.rb
362
376
  - spec/bard/git_spec.rb
363
377
  - spec/bard/github_pages_spec.rb
data/cucumber.yml DELETED
@@ -1 +0,0 @@
1
- default: --publish-quiet
@@ -1,12 +0,0 @@
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
@@ -1,13 +0,0 @@
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"
data/features/run.feature DELETED
@@ -1,13 +0,0 @@
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"
@@ -1,39 +0,0 @@
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,215 +0,0 @@
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