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.
- checksums.yaml +4 -4
- data/ARCHITECTURE.md +957 -0
- data/CUSTOM_STRATEGIES.md +701 -0
- data/MIGRATION_GUIDE.md +498 -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 +118 -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/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/dynamic_dsl_spec.rb +126 -0
- data/spec/bard/ssh_server_spec.rb +169 -0
- data/spec/bard/target_spec.rb +239 -0
- metadata +24 -2
data/lib/bard/target.rb
ADDED
|
@@ -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
|
@@ -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
|
data/spec/bard/config_spec.rb
CHANGED
|
@@ -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
|
|
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
|