bard 1.7.3 → 1.8.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.
- checksums.yaml +4 -4
- data/ARCHITECTURE.md +957 -0
- data/CUSTOM_STRATEGIES.md +701 -0
- data/MIGRATION_GUIDE.md +513 -0
- data/README.md +489 -0
- data/lib/bard/cli/deploy.rb +12 -3
- data/lib/bard/command.rb +25 -9
- data/lib/bard/config.rb +120 -43
- data/lib/bard/copy.rb +57 -13
- data/lib/bard/default_config.rb +35 -0
- data/lib/bard/deploy_strategy/github_pages.rb +135 -0
- data/lib/bard/deploy_strategy/ssh.rb +19 -0
- data/lib/bard/deploy_strategy.rb +60 -0
- data/lib/bard/deprecation.rb +19 -0
- data/lib/bard/github_pages.rb +49 -28
- data/lib/bard/server.rb +39 -1
- data/lib/bard/ssh_server.rb +100 -0
- data/lib/bard/target.rb +239 -0
- data/lib/bard/version.rb +1 -1
- data/spec/bard/capability_spec.rb +97 -0
- data/spec/bard/config_spec.rb +1 -1
- data/spec/bard/deploy_strategy/ssh_spec.rb +67 -0
- data/spec/bard/deploy_strategy_spec.rb +107 -0
- data/spec/bard/deprecation_spec.rb +202 -0
- data/spec/bard/dynamic_dsl_spec.rb +126 -0
- data/spec/bard/github_pages_spec.rb +64 -1
- data/spec/bard/ssh_server_spec.rb +169 -0
- data/spec/bard/target_spec.rb +239 -0
- metadata +27 -2
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
require "bard/deprecation"
|
|
2
|
+
require "bard/config"
|
|
3
|
+
require "bard/server"
|
|
4
|
+
|
|
5
|
+
describe Bard::Deprecation do
|
|
6
|
+
before do
|
|
7
|
+
Bard::Deprecation.reset!
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
describe ".warn" do
|
|
11
|
+
it "outputs a deprecation warning to stderr" do
|
|
12
|
+
expect {
|
|
13
|
+
Bard::Deprecation.warn "test message"
|
|
14
|
+
}.to output(/\[DEPRECATION\] test message/).to_stderr
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "includes the callsite location" do
|
|
18
|
+
output = capture_stderr { Bard::Deprecation.warn "test message" }
|
|
19
|
+
expect(output).to match(/called from.*deprecation_spec\.rb/)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "only warns once per callsite" do
|
|
23
|
+
output = capture_stderr do
|
|
24
|
+
3.times { Bard::Deprecation.warn "repeated message" }
|
|
25
|
+
end
|
|
26
|
+
expect(output.scan(/\[DEPRECATION\]/).count).to eq 1
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "warns separately for different callsites" do
|
|
30
|
+
output = capture_stderr do
|
|
31
|
+
Bard::Deprecation.warn "message 1"
|
|
32
|
+
Bard::Deprecation.warn "message 2"
|
|
33
|
+
end
|
|
34
|
+
expect(output.scan(/\[DEPRECATION\]/).count).to eq 2
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe ".reset!" do
|
|
39
|
+
it "clears the warning cache" do
|
|
40
|
+
capture_stderr { Bard::Deprecation.warn "test" }
|
|
41
|
+
Bard::Deprecation.reset!
|
|
42
|
+
output = capture_stderr { Bard::Deprecation.warn "test" }
|
|
43
|
+
expect(output).to include("[DEPRECATION]")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def capture_stderr
|
|
48
|
+
original_stderr = $stderr
|
|
49
|
+
$stderr = StringIO.new
|
|
50
|
+
yield
|
|
51
|
+
$stderr.string
|
|
52
|
+
ensure
|
|
53
|
+
$stderr = original_stderr
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
describe "Deprecation warnings" do
|
|
58
|
+
before do
|
|
59
|
+
Bard::Deprecation.reset!
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def capture_stderr
|
|
63
|
+
original_stderr = $stderr
|
|
64
|
+
$stderr = StringIO.new
|
|
65
|
+
yield
|
|
66
|
+
$stderr.string
|
|
67
|
+
ensure
|
|
68
|
+
$stderr = original_stderr
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
describe "Config#server" do
|
|
72
|
+
it "warns when using server instead of target" do
|
|
73
|
+
output = capture_stderr do
|
|
74
|
+
Bard::Config.new("test", source: <<~SOURCE)
|
|
75
|
+
server :production do
|
|
76
|
+
ssh "user@host:22"
|
|
77
|
+
end
|
|
78
|
+
SOURCE
|
|
79
|
+
end
|
|
80
|
+
expect(output).to include("[DEPRECATION]")
|
|
81
|
+
expect(output).to include("`server` is deprecated")
|
|
82
|
+
expect(output).to include("use `target` instead")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "does not warn when using target" do
|
|
86
|
+
output = capture_stderr do
|
|
87
|
+
Bard::Config.new("test", source: <<~SOURCE)
|
|
88
|
+
target :production do
|
|
89
|
+
ssh "user@host:22"
|
|
90
|
+
end
|
|
91
|
+
SOURCE
|
|
92
|
+
end
|
|
93
|
+
expect(output).not_to include("[DEPRECATION]")
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
describe "Server SSH options" do
|
|
98
|
+
it "warns when using separate path method" do
|
|
99
|
+
output = capture_stderr do
|
|
100
|
+
Bard::Server.define("test", :production) do
|
|
101
|
+
ssh "user@host:22"
|
|
102
|
+
path "/app"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
expect(output).to include("[DEPRECATION]")
|
|
106
|
+
expect(output).to include("Separate SSH options are deprecated")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "warns when using separate gateway method" do
|
|
110
|
+
output = capture_stderr do
|
|
111
|
+
Bard::Server.define("test", :production) do
|
|
112
|
+
ssh "user@host:22"
|
|
113
|
+
gateway "bastion@host:22"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
expect(output).to include("[DEPRECATION]")
|
|
117
|
+
expect(output).to include("Separate SSH options are deprecated")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "warns when using separate ssh_key method" do
|
|
121
|
+
output = capture_stderr do
|
|
122
|
+
Bard::Server.define("test", :production) do
|
|
123
|
+
ssh "user@host:22"
|
|
124
|
+
ssh_key "~/.ssh/id_rsa"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
expect(output).to include("[DEPRECATION]")
|
|
128
|
+
expect(output).to include("Separate SSH options are deprecated")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "warns when using separate env method" do
|
|
132
|
+
output = capture_stderr do
|
|
133
|
+
Bard::Server.define("test", :production) do
|
|
134
|
+
ssh "user@host:22"
|
|
135
|
+
env "RAILS_ENV=production"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
expect(output).to include("[DEPRECATION]")
|
|
139
|
+
expect(output).to include("Separate SSH options are deprecated")
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
describe "Server strategy configuration" do
|
|
144
|
+
it "warns when using strategy method" do
|
|
145
|
+
output = capture_stderr do
|
|
146
|
+
Bard::Server.define("test", :production) do
|
|
147
|
+
ssh "user@host:22"
|
|
148
|
+
strategy :custom
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
expect(output).to include("[DEPRECATION]")
|
|
152
|
+
expect(output).to include("`strategy` is deprecated")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "warns when using option method" do
|
|
156
|
+
output = capture_stderr do
|
|
157
|
+
Bard::Server.define("test", :production) do
|
|
158
|
+
ssh "user@host:22"
|
|
159
|
+
option :run_tests, true
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
expect(output).to include("[DEPRECATION]")
|
|
163
|
+
expect(output).to include("`option` is deprecated")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it "stores strategy name for backward compatibility" do
|
|
167
|
+
server = nil
|
|
168
|
+
capture_stderr do
|
|
169
|
+
server = Bard::Server.define("test", :production) do
|
|
170
|
+
ssh "user@host:22"
|
|
171
|
+
strategy :jets
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
expect(server.strategy_name).to eq :jets
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "stores strategy options for backward compatibility" do
|
|
178
|
+
server = nil
|
|
179
|
+
capture_stderr do
|
|
180
|
+
server = Bard::Server.define("test", :production) do
|
|
181
|
+
ssh "user@host:22"
|
|
182
|
+
option :run_tests, true
|
|
183
|
+
option :verbose, false
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
expect(server.strategy_options).to eq({ run_tests: true, verbose: false })
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
describe "Target (new API)" do
|
|
191
|
+
it "does not warn when using hash options with ssh" do
|
|
192
|
+
output = capture_stderr do
|
|
193
|
+
Bard::Config.new("test", source: <<~SOURCE)
|
|
194
|
+
target :production do
|
|
195
|
+
ssh "user@host:22", path: "/app", gateway: "bastion@host:22"
|
|
196
|
+
end
|
|
197
|
+
SOURCE
|
|
198
|
+
end
|
|
199
|
+
expect(output).not_to include("[DEPRECATION]")
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "bard/target"
|
|
3
|
+
require "bard/deploy_strategy"
|
|
4
|
+
|
|
5
|
+
describe "Dynamic DSL Methods" do
|
|
6
|
+
let(:config) { double("config", project_name: "testapp") }
|
|
7
|
+
let(:target) { Bard::Target.new(:production, config) }
|
|
8
|
+
|
|
9
|
+
before do
|
|
10
|
+
# Register test strategies
|
|
11
|
+
class Bard::DeployStrategy::Jets < Bard::DeployStrategy
|
|
12
|
+
def deploy
|
|
13
|
+
# test implementation
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class Bard::DeployStrategy::Docker < Bard::DeployStrategy
|
|
18
|
+
def deploy
|
|
19
|
+
# test implementation
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe "method_missing for strategies" do
|
|
25
|
+
it "enables strategy when method name matches registered strategy" do
|
|
26
|
+
target.jets("https://api.example.com")
|
|
27
|
+
expect(target.deploy_strategy).to eq(:jets)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "stores strategy options" do
|
|
31
|
+
target.jets("https://api.example.com", run_tests: true, env: "production")
|
|
32
|
+
options = target.strategy_options(:jets)
|
|
33
|
+
expect(options[:run_tests]).to be true
|
|
34
|
+
expect(options[:env]).to eq("production")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "auto-configures ping URL from first argument if it's a URL" do
|
|
38
|
+
target.jets("https://api.example.com")
|
|
39
|
+
expect(target.ping_urls).to include("https://api.example.com")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "works with multiple strategies" do
|
|
43
|
+
target1 = Bard::Target.new(:production, config)
|
|
44
|
+
target2 = Bard::Target.new(:staging, config)
|
|
45
|
+
|
|
46
|
+
target1.jets("https://api.example.com")
|
|
47
|
+
target2.docker("https://app.example.com")
|
|
48
|
+
|
|
49
|
+
expect(target1.deploy_strategy).to eq(:jets)
|
|
50
|
+
expect(target2.deploy_strategy).to eq(:docker)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "raises NoMethodError for unknown methods" do
|
|
54
|
+
expect { target.unknown_method("arg") }
|
|
55
|
+
.to raise_error(NoMethodError)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
describe "strategy DSL integration" do
|
|
60
|
+
it "allows chaining with other configuration methods" do
|
|
61
|
+
target.jets("https://api.example.com", run_tests: true)
|
|
62
|
+
target.ssh("deploy@example.com:22", path: "app")
|
|
63
|
+
|
|
64
|
+
expect(target.deploy_strategy).to eq(:jets)
|
|
65
|
+
expect(target.has_capability?(:ssh)).to be true
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "allows strategy configuration without ping URL" do
|
|
69
|
+
target.docker(skip_build: true)
|
|
70
|
+
options = target.strategy_options(:docker)
|
|
71
|
+
expect(options[:skip_build]).to be true
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe "#strategy_options" do
|
|
76
|
+
it "returns options for the specified strategy" do
|
|
77
|
+
target.jets("https://api.example.com", run_tests: true, env: "prod")
|
|
78
|
+
options = target.strategy_options(:jets)
|
|
79
|
+
expect(options[:run_tests]).to be true
|
|
80
|
+
expect(options[:env]).to eq("prod")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "returns empty hash if strategy not configured" do
|
|
84
|
+
options = target.strategy_options(:unknown)
|
|
85
|
+
expect(options).to eq({})
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "filters out URL from options" do
|
|
89
|
+
target.jets("https://api.example.com", run_tests: true)
|
|
90
|
+
options = target.strategy_options(:jets)
|
|
91
|
+
expect(options[:run_tests]).to be true
|
|
92
|
+
expect(options).not_to have_key(:url)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
describe "#deploy_strategy" do
|
|
97
|
+
it "returns the configured strategy symbol" do
|
|
98
|
+
target.jets("https://api.example.com")
|
|
99
|
+
expect(target.deploy_strategy).to eq(:jets)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "returns nil if no strategy configured" do
|
|
103
|
+
expect(target.deploy_strategy).to be_nil
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
describe "#deploy_strategy_instance" do
|
|
108
|
+
it "creates an instance of the strategy class" do
|
|
109
|
+
target.jets("https://api.example.com")
|
|
110
|
+
instance = target.deploy_strategy_instance
|
|
111
|
+
expect(instance).to be_a(Bard::DeployStrategy::Jets)
|
|
112
|
+
expect(instance.target).to eq(target)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "raises error if no strategy configured" do
|
|
116
|
+
expect { target.deploy_strategy_instance }
|
|
117
|
+
.to raise_error(/No deployment strategy configured/)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "raises error if strategy class not found" do
|
|
121
|
+
target.instance_variable_set(:@deploy_strategy, :unknown)
|
|
122
|
+
expect { target.deploy_strategy_instance }
|
|
123
|
+
.to raise_error(/Unknown deployment strategy: unknown/)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -38,6 +38,69 @@ describe Bard::GithubPages do
|
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
describe "#build_site" do
|
|
42
|
+
it "uses the locked port" do
|
|
43
|
+
github_pages.instance_variable_set(:@sha, "abc123")
|
|
44
|
+
github_pages.instance_variable_set(:@build_dir, "tmp/github-build-abc123")
|
|
45
|
+
github_pages.instance_variable_set(:@domain, "example.com")
|
|
46
|
+
|
|
47
|
+
allow(github_pages).to receive(:with_locked_port).and_yield(3005)
|
|
48
|
+
|
|
49
|
+
expect(github_pages).to receive(:run!).with(satisfy { |cmd|
|
|
50
|
+
cmd.include?("rails s -p 3005") && cmd.include?("http://localhost:3005")
|
|
51
|
+
}).ordered
|
|
52
|
+
|
|
53
|
+
expect(github_pages).to receive(:run!).with(include("kill")).ordered
|
|
54
|
+
|
|
55
|
+
github_pages.send(:build_site)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
describe "#with_locked_port" do
|
|
60
|
+
let(:file_mock) { double("file", close: true) }
|
|
61
|
+
|
|
62
|
+
before do
|
|
63
|
+
allow(File).to receive(:open).and_return(file_mock)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "yields the first available port" do
|
|
67
|
+
allow(file_mock).to receive(:flock).and_return(true)
|
|
68
|
+
|
|
69
|
+
expect(File).to receive(:open).with("/tmp/bard_github_pages_3000.lock", anything, anything)
|
|
70
|
+
|
|
71
|
+
yielded_port = nil
|
|
72
|
+
github_pages.send(:with_locked_port) { |p| yielded_port = p }
|
|
73
|
+
expect(yielded_port).to eq(3000)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "retries if the first port is locked" do
|
|
77
|
+
# 1. Try port 3000
|
|
78
|
+
expect(File).to receive(:open).with("/tmp/bard_github_pages_3000.lock", anything, anything).ordered
|
|
79
|
+
expect(file_mock).to receive(:flock).with(File::LOCK_EX | File::LOCK_NB).and_return(false).ordered
|
|
80
|
+
expect(file_mock).to receive(:close).ordered
|
|
81
|
+
|
|
82
|
+
# 2. Try port 3001
|
|
83
|
+
expect(File).to receive(:open).with("/tmp/bard_github_pages_3001.lock", anything, anything).ordered
|
|
84
|
+
expect(file_mock).to receive(:flock).with(File::LOCK_EX | File::LOCK_NB).and_return(true).ordered
|
|
85
|
+
|
|
86
|
+
# 3. Cleanup after yielding
|
|
87
|
+
expect(file_mock).to receive(:flock).with(File::LOCK_UN).ordered
|
|
88
|
+
expect(file_mock).to receive(:close).ordered
|
|
89
|
+
|
|
90
|
+
yielded_port = nil
|
|
91
|
+
github_pages.send(:with_locked_port) { |p| yielded_port = p }
|
|
92
|
+
expect(yielded_port).to eq(3001)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "raises an error if no ports are available" do
|
|
96
|
+
allow(file_mock).to receive(:flock).and_return(false)
|
|
97
|
+
|
|
98
|
+
expect {
|
|
99
|
+
github_pages.send(:with_locked_port) {}
|
|
100
|
+
}.to raise_error(/Could not find an available port/)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
41
104
|
describe "#get_parent_commit" do
|
|
42
105
|
it "returns the sha of the gh-pages branch" do
|
|
43
106
|
github_pages.instance_variable_set(:@branch, "gh-pages")
|
|
@@ -77,4 +140,4 @@ describe Bard::GithubPages do
|
|
|
77
140
|
end
|
|
78
141
|
end
|
|
79
142
|
end
|
|
80
|
-
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "bard/ssh_server"
|
|
3
|
+
|
|
4
|
+
describe Bard::SSHServer do
|
|
5
|
+
describe "#initialize" do
|
|
6
|
+
it "parses SSH URI with user, host, and port" do
|
|
7
|
+
server = described_class.new("deploy@example.com:22")
|
|
8
|
+
expect(server.user).to eq("deploy")
|
|
9
|
+
expect(server.host).to eq("example.com")
|
|
10
|
+
expect(server.port).to eq("22")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "handles SSH URI without port (defaults to 22)" do
|
|
14
|
+
server = described_class.new("deploy@example.com")
|
|
15
|
+
expect(server.user).to eq("deploy")
|
|
16
|
+
expect(server.host).to eq("example.com")
|
|
17
|
+
expect(server.port).to eq("22")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "handles SSH URI without user (uses current user)" do
|
|
21
|
+
server = described_class.new("example.com:22")
|
|
22
|
+
expect(server.host).to eq("example.com")
|
|
23
|
+
expect(server.port).to eq("22")
|
|
24
|
+
expect(server.user).to eq(ENV['USER'])
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "accepts options hash" do
|
|
28
|
+
server = described_class.new("deploy@example.com:22",
|
|
29
|
+
path: "/app",
|
|
30
|
+
gateway: "bastion@example.com:22",
|
|
31
|
+
ssh_key: "/path/to/key",
|
|
32
|
+
env: "RAILS_ENV=production"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
expect(server.path).to eq("/app")
|
|
36
|
+
expect(server.gateway).to eq("bastion@example.com:22")
|
|
37
|
+
expect(server.ssh_key).to eq("/path/to/key")
|
|
38
|
+
expect(server.env).to eq("RAILS_ENV=production")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe "#ssh_uri" do
|
|
43
|
+
it "returns the SSH connection string" do
|
|
44
|
+
server = described_class.new("deploy@example.com:22")
|
|
45
|
+
expect(server.ssh_uri).to eq("deploy@example.com:22")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "includes port if non-standard" do
|
|
49
|
+
server = described_class.new("deploy@example.com:2222")
|
|
50
|
+
expect(server.ssh_uri).to eq("deploy@example.com:2222")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe "#hostname" do
|
|
55
|
+
it "extracts hostname from SSH URI" do
|
|
56
|
+
server = described_class.new("deploy@example.com:22")
|
|
57
|
+
expect(server.hostname).to eq("example.com")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "handles IP addresses" do
|
|
61
|
+
server = described_class.new("deploy@192.168.1.1:22")
|
|
62
|
+
expect(server.hostname).to eq("192.168.1.1")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe "#connection_string" do
|
|
67
|
+
it "builds SSH connection string" do
|
|
68
|
+
server = described_class.new("deploy@example.com:22")
|
|
69
|
+
expect(server.connection_string).to eq("deploy@example.com")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe "#run" do
|
|
74
|
+
let(:server) do
|
|
75
|
+
described_class.new("deploy@example.com:22", path: "/app")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "executes command via SSH" do
|
|
79
|
+
expect(Open3).to receive(:capture3)
|
|
80
|
+
.with(/ssh.*deploy@example.com.*cd \/app && ls/)
|
|
81
|
+
.and_return(["output", "", 0])
|
|
82
|
+
|
|
83
|
+
server.run("ls")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "includes environment variables if configured" do
|
|
87
|
+
server_with_env = described_class.new("deploy@example.com:22",
|
|
88
|
+
path: "/app",
|
|
89
|
+
env: "RAILS_ENV=production"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
expect(Open3).to receive(:capture3)
|
|
93
|
+
.with(/RAILS_ENV=production/)
|
|
94
|
+
.and_return(["output", "", 0])
|
|
95
|
+
|
|
96
|
+
server_with_env.run("ls")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe "#run!" do
|
|
101
|
+
let(:server) do
|
|
102
|
+
described_class.new("deploy@example.com:22", path: "/app")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "executes command via SSH" do
|
|
106
|
+
expect(Open3).to receive(:capture3)
|
|
107
|
+
.with(/ssh.*deploy@example.com.*cd \/app && ls/)
|
|
108
|
+
.and_return(["output", "", 0])
|
|
109
|
+
|
|
110
|
+
server.run!("ls")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it "raises error if command fails" do
|
|
114
|
+
expect(Open3).to receive(:capture3)
|
|
115
|
+
.and_return(["", "error", 1])
|
|
116
|
+
|
|
117
|
+
expect { server.run!("false") }.to raise_error(Bard::Command::Error)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
describe "#exec!" do
|
|
122
|
+
let(:server) do
|
|
123
|
+
described_class.new("deploy@example.com:22", path: "/app")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it "replaces current process with SSH command" do
|
|
127
|
+
expect(server).to receive(:exec)
|
|
128
|
+
.with(/ssh.*deploy@example.com.*cd \/app && ls/)
|
|
129
|
+
|
|
130
|
+
server.exec!("ls")
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
describe "path handling" do
|
|
135
|
+
it "uses path in commands if configured" do
|
|
136
|
+
server = described_class.new("deploy@example.com:22", path: "/var/www/app")
|
|
137
|
+
|
|
138
|
+
expect(Open3).to receive(:capture3)
|
|
139
|
+
.with(/cd \/var\/www\/app && ls/)
|
|
140
|
+
.and_return(["output", "", 0])
|
|
141
|
+
|
|
142
|
+
server.run("ls")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it "works without path" do
|
|
146
|
+
server = described_class.new("deploy@example.com:22")
|
|
147
|
+
|
|
148
|
+
expect(Open3).to receive(:capture3)
|
|
149
|
+
.with(/ssh.*ls/)
|
|
150
|
+
.and_return(["output", "", 0])
|
|
151
|
+
|
|
152
|
+
server.run("ls")
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
describe "gateway/bastion support" do
|
|
157
|
+
it "uses ProxyJump for gateway" do
|
|
158
|
+
server = described_class.new("deploy@private.example.com:22",
|
|
159
|
+
gateway: "bastion@public.example.com:22"
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
expect(Open3).to receive(:capture3)
|
|
163
|
+
.with(/-o ProxyJump=bastion@public.example.com:22/)
|
|
164
|
+
.and_return(["output", "", 0])
|
|
165
|
+
|
|
166
|
+
server.run("ls")
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|