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.
@@ -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