bard 1.7.4 → 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.
@@ -0,0 +1,239 @@
1
+ require "uri"
2
+ require "bard/command"
3
+ require "bard/copy"
4
+ require "bard/deploy_strategy"
5
+
6
+ module Bard
7
+ class Target
8
+ attr_reader :key, :config, :path
9
+ attr_accessor :server, :gateway, :ssh_key, :env
10
+
11
+ def initialize(key, config)
12
+ @key = key
13
+ @config = config
14
+ @capabilities = []
15
+ @ping_urls = []
16
+ @strategy_options_hash = {}
17
+ @deploy_strategy = nil
18
+ @path = nil
19
+ @server = nil
20
+ end
21
+
22
+ # Capability tracking
23
+ def enable_capability(capability)
24
+ @capabilities << capability unless @capabilities.include?(capability)
25
+ end
26
+
27
+ def has_capability?(capability)
28
+ @capabilities.include?(capability)
29
+ end
30
+
31
+ def require_capability!(capability)
32
+ unless has_capability?(capability)
33
+ error_message = case capability
34
+ when :ssh
35
+ "SSH not configured for this target"
36
+ when :ping
37
+ "Ping URL not configured for this target"
38
+ else
39
+ "#{capability} capability not configured for this target"
40
+ end
41
+ raise error_message
42
+ end
43
+ end
44
+
45
+ # SSH configuration
46
+ def ssh(uri_or_false = nil, **options)
47
+ if uri_or_false.nil?
48
+ # Getter - return false if explicitly disabled, otherwise return server
49
+ return @ssh_disabled ? false : @server
50
+ elsif uri_or_false == false
51
+ # Disable SSH
52
+ @server = nil
53
+ @ssh_disabled = true
54
+ @capabilities.delete(:ssh)
55
+ else
56
+ # Enable SSH
57
+ require "bard/ssh_server"
58
+ @server = SSHServer.new(uri_or_false, **options)
59
+ @path = options[:path] if options[:path]
60
+ @gateway = options[:gateway] if options[:gateway]
61
+ @ssh_key = options[:ssh_key] if options[:ssh_key]
62
+ @env = options[:env] if options[:env]
63
+ enable_capability(:ssh)
64
+
65
+ # Set SSH as default deployment strategy if none set
66
+ @deploy_strategy ||= :ssh
67
+
68
+ # Auto-configure ping from hostname
69
+ hostname = @server.hostname
70
+ ping(hostname) if hostname
71
+ end
72
+ end
73
+
74
+ def ssh_uri
75
+ server&.ssh_uri
76
+ end
77
+
78
+ # Path configuration
79
+ def path(new_path = nil)
80
+ if new_path
81
+ @path = new_path
82
+ else
83
+ @path || config.project_name
84
+ end
85
+ end
86
+
87
+ # Ping configuration
88
+ def ping(*urls)
89
+ if urls.empty?
90
+ # Getter
91
+ @ping_urls
92
+ elsif urls.first == false
93
+ # Disable ping
94
+ @ping_urls = []
95
+ @capabilities.delete(:ping)
96
+ else
97
+ # Enable ping
98
+ @ping_urls = urls.flatten
99
+ enable_capability(:ping)
100
+ end
101
+ end
102
+
103
+ def ping_urls
104
+ @ping_urls
105
+ end
106
+
107
+ def ping!
108
+ require_capability!(:ping)
109
+ require "bard/ping"
110
+ failed_urls = Bard::Ping.call(self)
111
+ if failed_urls.any?
112
+ raise "Ping failed for: #{failed_urls.join(', ')}"
113
+ end
114
+ end
115
+
116
+ def open
117
+ require_capability!(:ping)
118
+ system "open #{ping_urls.first}"
119
+ end
120
+
121
+ # Deploy strategy
122
+ attr_reader :deploy_strategy
123
+
124
+ # GitHub Pages deployment configuration
125
+ def github_pages(url = nil)
126
+ if url.nil?
127
+ # Getter
128
+ @github_pages_url
129
+ else
130
+ # Setter
131
+ @deploy_strategy = :github_pages
132
+ @github_pages_url = url
133
+ enable_capability(:github_pages)
134
+ end
135
+ end
136
+
137
+ def strategy_options(strategy_name)
138
+ @strategy_options_hash[strategy_name] || {}
139
+ end
140
+
141
+ def deploy_strategy_instance
142
+ raise "No deployment strategy configured for target #{key}" unless @deploy_strategy
143
+
144
+ strategy_class = DeployStrategy[@deploy_strategy]
145
+ raise "Unknown deployment strategy: #{@deploy_strategy}" unless strategy_class
146
+
147
+ strategy_class.new(self)
148
+ end
149
+
150
+ # Dynamic strategy DSL via method_missing
151
+ def method_missing(method, *args, **kwargs, &block)
152
+ strategy_class = DeployStrategy[method]
153
+
154
+ if strategy_class
155
+ # This is a deployment strategy
156
+ @deploy_strategy = method
157
+
158
+ # Store options
159
+ @strategy_options_hash[method] = kwargs
160
+
161
+ # Auto-configure ping if first arg is a URL
162
+ if args.first && args.first.to_s =~ /^https?:\/\//
163
+ ping(args.first)
164
+ end
165
+
166
+ # Call the strategy's initializer if it wants to configure the target
167
+ # (This will be handled by the strategy class)
168
+ else
169
+ super
170
+ end
171
+ end
172
+
173
+ def respond_to_missing?(method, include_private = false)
174
+ DeployStrategy[method] || super
175
+ end
176
+
177
+ # Remote command execution
178
+ def run!(command, home: false, verbose: false, quiet: false)
179
+ require_capability!(:ssh)
180
+ Command.run!(command, on: server, home: home, verbose: verbose, quiet: quiet)
181
+ end
182
+
183
+ def run(command, home: false, verbose: false, quiet: false)
184
+ require_capability!(:ssh)
185
+ Command.run(command, on: server, home: home, verbose: verbose, quiet: quiet)
186
+ end
187
+
188
+ def exec!(command, home: false)
189
+ require_capability!(:ssh)
190
+ Command.exec!(command, on: server, home: home)
191
+ end
192
+
193
+ # File transfer
194
+ def copy_file(path, to:, verbose: false)
195
+ require_capability!(:ssh)
196
+ to.require_capability!(:ssh)
197
+ Copy.file(path, from: self, to: to, verbose: verbose)
198
+ end
199
+
200
+ def copy_dir(path, to:, verbose: false)
201
+ require_capability!(:ssh)
202
+ to.require_capability!(:ssh)
203
+ Copy.dir(path, from: self, to: to, verbose: verbose)
204
+ end
205
+
206
+ # URI methods for compatibility
207
+ 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
212
+ end
213
+
214
+ def rsync_uri(file_path = nil)
215
+ uri = URI("ssh://#{ssh_uri}")
216
+ str = "#{uri.user}@#{uri.host}"
217
+ str += ":#{path}"
218
+ str += "/#{file_path}" if file_path
219
+ str
220
+ end
221
+
222
+ # Utility methods
223
+ def to_s
224
+ key.to_s
225
+ end
226
+
227
+ def to_sym
228
+ key
229
+ end
230
+
231
+ def with(attrs)
232
+ dup.tap do |t|
233
+ attrs.each do |key, value|
234
+ t.send(key, value)
235
+ end
236
+ end
237
+ end
238
+ end
239
+ end
data/lib/bard/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Bard
2
- VERSION = "1.7.4"
2
+ VERSION = "2.0.0.beta"
3
3
  end
4
4
 
@@ -0,0 +1,97 @@
1
+ require "spec_helper"
2
+ require "bard/target"
3
+
4
+ describe "Capability System" do
5
+ let(:config) { double("config", project_name: "testapp") }
6
+ let(:target) { Bard::Target.new(:production, config) }
7
+
8
+ describe "#enable_capability" do
9
+ it "enables a capability on the target" do
10
+ target.enable_capability(:ssh)
11
+ expect(target.has_capability?(:ssh)).to be true
12
+ end
13
+
14
+ it "can enable multiple capabilities" do
15
+ target.enable_capability(:ssh)
16
+ target.enable_capability(:ping)
17
+ expect(target.has_capability?(:ssh)).to be true
18
+ expect(target.has_capability?(:ping)).to be true
19
+ end
20
+ end
21
+
22
+ describe "#has_capability?" do
23
+ it "returns false for capabilities that are not enabled" do
24
+ expect(target.has_capability?(:ssh)).to be false
25
+ end
26
+
27
+ it "returns true for capabilities that are enabled" do
28
+ target.enable_capability(:ssh)
29
+ expect(target.has_capability?(:ssh)).to be true
30
+ end
31
+ end
32
+
33
+ describe "#require_capability!" do
34
+ it "does not raise an error if capability is enabled" do
35
+ target.enable_capability(:ssh)
36
+ expect { target.require_capability!(:ssh) }.not_to raise_error
37
+ end
38
+
39
+ it "raises an error if capability is not enabled" do
40
+ expect { target.require_capability!(:ssh) }
41
+ .to raise_error(/SSH not configured for this target/)
42
+ end
43
+
44
+ it "provides custom error message for ping capability" do
45
+ expect { target.require_capability!(:ping) }
46
+ .to raise_error(/Ping URL not configured for this target/)
47
+ end
48
+
49
+ it "provides generic error message for unknown capabilities" do
50
+ expect { target.require_capability!(:unknown) }
51
+ .to raise_error(/unknown capability not configured for this target/)
52
+ end
53
+ end
54
+
55
+ describe "capability dependency checking" do
56
+ context "SSH-dependent methods" do
57
+ it "run! requires SSH capability" do
58
+ expect { target.run!("ls") }
59
+ .to raise_error(/SSH not configured/)
60
+ end
61
+
62
+ it "run requires SSH capability" do
63
+ expect { target.run("ls") }
64
+ .to raise_error(/SSH not configured/)
65
+ end
66
+
67
+ it "exec! requires SSH capability" do
68
+ expect { target.exec!("ls") }
69
+ .to raise_error(/SSH not configured/)
70
+ end
71
+
72
+ it "copy_file requires SSH capability" do
73
+ other_target = Bard::Target.new(:staging, config)
74
+ expect { target.copy_file("test.txt", to: other_target) }
75
+ .to raise_error(/SSH not configured/)
76
+ end
77
+
78
+ it "copy_dir requires SSH capability" do
79
+ other_target = Bard::Target.new(:staging, config)
80
+ expect { target.copy_dir("test/", to: other_target) }
81
+ .to raise_error(/SSH not configured/)
82
+ end
83
+ end
84
+
85
+ context "Ping-dependent methods" do
86
+ it "ping! requires ping capability" do
87
+ expect { target.ping! }
88
+ .to raise_error(/Ping URL not configured/)
89
+ end
90
+
91
+ it "open requires ping capability" do
92
+ expect { target.open }
93
+ .to raise_error(/Ping URL not configured/)
94
+ end
95
+ end
96
+ end
97
+ end
@@ -161,7 +161,7 @@ describe Bard::Config do
161
161
  it "creates a production server with github_pages enabled" do
162
162
  production = subject[:production]
163
163
  expect(production).not_to be_nil
164
- expect(production.github_pages).to eq true
164
+ expect(production.github_pages).to eq "example.com"
165
165
  expect(production.ssh).to eq false
166
166
  end
167
167
  end
@@ -0,0 +1,67 @@
1
+ require "spec_helper"
2
+ require "bard/deploy_strategy"
3
+ require "bard/deploy_strategy/ssh"
4
+
5
+ describe Bard::DeployStrategy::SSH do
6
+ let(:config) { double("config", project_name: "testapp") }
7
+ let(:target) do
8
+ t = Bard::Target.new(:production, config)
9
+ t.ssh("deploy@example.com:22", path: "/app")
10
+ t
11
+ end
12
+ let(:strategy) { described_class.new(target) }
13
+
14
+ describe "#deploy" do
15
+ it "requires SSH capability" do
16
+ target_without_ssh = Bard::Target.new(:local, config)
17
+ strategy_without_ssh = described_class.new(target_without_ssh)
18
+
19
+ expect { strategy_without_ssh.deploy }
20
+ .to raise_error(/SSH not configured/)
21
+ end
22
+
23
+ it "runs git pull on remote server" do
24
+ expect(target).to receive(:run!)
25
+ .with(/git pull origin master/)
26
+
27
+ allow(target).to receive(:run!).with(/bin\/setup/)
28
+
29
+ strategy.deploy
30
+ end
31
+
32
+ it "runs bin/setup on remote server" do
33
+ allow(target).to receive(:run!).with(/git pull/)
34
+
35
+ expect(target).to receive(:run!)
36
+ .with(/bin\/setup/)
37
+
38
+ strategy.deploy
39
+ end
40
+
41
+ it "uses configured branch if specified" do
42
+ target.instance_variable_set(:@branch, "main")
43
+
44
+ expect(target).to receive(:run!)
45
+ .with(/git pull origin main/)
46
+
47
+ allow(target).to receive(:run!).with(/bin\/setup/)
48
+
49
+ strategy.deploy
50
+ end
51
+ end
52
+
53
+ describe "auto-registration" do
54
+ it "registers as :ssh strategy" do
55
+ expect(Bard::DeployStrategy[:ssh]).to eq(described_class)
56
+ end
57
+ end
58
+
59
+ describe "integration with target" do
60
+ it "is enabled by ssh DSL method" do
61
+ new_target = Bard::Target.new(:staging, config)
62
+ new_target.ssh("deploy@staging.example.com:22")
63
+
64
+ expect(new_target.deploy_strategy).to eq(:ssh)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,107 @@
1
+ require "spec_helper"
2
+ require "bard/deploy_strategy"
3
+
4
+ describe Bard::DeployStrategy do
5
+ describe "auto-registration" do
6
+ it "registers strategies via inherited hook" do
7
+ # Define a test strategy
8
+ class Bard::DeployStrategy::TestStrategy < Bard::DeployStrategy
9
+ end
10
+
11
+ expect(Bard::DeployStrategy[:test_strategy]).to eq(Bard::DeployStrategy::TestStrategy)
12
+ end
13
+
14
+ it "extracts strategy name from class name" do
15
+ class Bard::DeployStrategy::MyCustomStrategy < Bard::DeployStrategy
16
+ end
17
+
18
+ expect(Bard::DeployStrategy[:my_custom_strategy]).to eq(Bard::DeployStrategy::MyCustomStrategy)
19
+ end
20
+
21
+
22
+ it "allows retrieval of registered strategies" do
23
+ class Bard::DeployStrategy::RetrievalTest < Bard::DeployStrategy
24
+ end
25
+
26
+ strategy_class = Bard::DeployStrategy[:retrieval_test]
27
+ expect(strategy_class).to eq(Bard::DeployStrategy::RetrievalTest)
28
+ expect(strategy_class.superclass).to eq(Bard::DeployStrategy)
29
+ end
30
+ end
31
+
32
+ describe ".strategies" do
33
+ it "returns a hash of all registered strategies" do
34
+ class Bard::DeployStrategy::Strategy1 < Bard::DeployStrategy
35
+ end
36
+ class Bard::DeployStrategy::Strategy2 < Bard::DeployStrategy
37
+ end
38
+
39
+ strategies = Bard::DeployStrategy.strategies
40
+ expect(strategies).to be_a(Hash)
41
+ expect(strategies[:strategy1]).to eq(Bard::DeployStrategy::Strategy1)
42
+ expect(strategies[:strategy2]).to eq(Bard::DeployStrategy::Strategy2)
43
+ end
44
+ end
45
+
46
+ describe ".[]" do
47
+ it "retrieves a strategy by symbol" do
48
+ class Bard::DeployStrategy::LookupTest < Bard::DeployStrategy
49
+ end
50
+
51
+ expect(Bard::DeployStrategy[:lookup_test]).to eq(Bard::DeployStrategy::LookupTest)
52
+ end
53
+
54
+ it "returns nil for unknown strategies" do
55
+ expect(Bard::DeployStrategy[:unknown_strategy]).to be_nil
56
+ end
57
+ end
58
+
59
+ describe "#initialize" do
60
+ let(:target) { double("target") }
61
+
62
+ it "stores the target" do
63
+ strategy = described_class.new(target)
64
+ expect(strategy.target).to eq(target)
65
+ end
66
+ end
67
+
68
+ describe "#deploy" do
69
+ let(:target) { double("target") }
70
+ let(:strategy) { described_class.new(target) }
71
+
72
+ it "raises NotImplementedError" do
73
+ expect { strategy.deploy }.to raise_error(NotImplementedError)
74
+ end
75
+ end
76
+
77
+ describe "helper methods" do
78
+ let(:target) { double("target") }
79
+ let(:strategy) { described_class.new(target) }
80
+
81
+ describe "#run!" do
82
+ it "delegates to Bard::Command.run!" do
83
+ expect(Bard::Command).to receive(:run!).with("ls -l")
84
+ strategy.run!("ls -l")
85
+ end
86
+ end
87
+
88
+ describe "#run" do
89
+ it "delegates to Bard::Command.run" do
90
+ expect(Bard::Command).to receive(:run).with("ls -l")
91
+ strategy.run("ls -l")
92
+ end
93
+ end
94
+
95
+ describe "#system!" do
96
+ it "delegates to Kernel.system with error checking" do
97
+ expect(Kernel).to receive(:system).with("ls -l").and_return(true)
98
+ strategy.system!("ls -l")
99
+ end
100
+
101
+ it "raises error if command fails" do
102
+ expect(Kernel).to receive(:system).with("false").and_return(false)
103
+ expect { strategy.system!("false") }.to raise_error(/Command failed/)
104
+ end
105
+ end
106
+ end
107
+ 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