bard 2.0.0.beta → 2.0.1
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/.github/workflows/ci.yml +6 -1
- data/CLAUDE.md +76 -0
- data/MIGRATION_GUIDE.md +24 -9
- data/PLUGINS.md +99 -0
- data/README.md +14 -6
- data/Rakefile +3 -1
- data/bard.gemspec +2 -1
- data/cucumber.yml +1 -0
- data/features/ci.feature +63 -0
- data/features/data.feature +13 -0
- data/features/deploy.feature +14 -0
- data/features/deploy_git_workflow.feature +89 -0
- data/features/run.feature +14 -0
- data/features/step_definitions/bard_steps.rb +136 -0
- data/features/support/bard-coverage +16 -0
- data/features/support/env.rb +14 -39
- data/features/support/test_server.rb +216 -0
- data/lib/bard/cli.rb +14 -31
- data/lib/bard/command.rb +10 -69
- data/lib/bard/config.rb +40 -183
- data/lib/bard/copy.rb +28 -103
- data/lib/bard/plugins/data.rb +56 -0
- data/lib/bard/{ci → plugins/deploy/ci}/github_actions.rb +3 -4
- data/lib/bard/plugins/deploy/ci/jenkins.rb +176 -0
- data/lib/bard/{ci → plugins/deploy/ci}/local.rb +7 -7
- data/lib/bard/{ci → plugins/deploy/ci}/runner.rb +38 -4
- data/lib/bard/plugins/deploy/ci.rb +38 -0
- data/lib/bard/plugins/deploy/ssh_strategy.rb +27 -0
- data/lib/bard/{deploy_strategy.rb → plugins/deploy/strategy.rb} +1 -1
- data/lib/bard/plugins/deploy.rb +240 -0
- data/lib/bard/{git.rb → plugins/git.rb} +6 -3
- data/lib/bard/{github.rb → plugins/github.rb} +4 -6
- data/lib/bard/{deploy_strategy/github_pages.rb → plugins/github_pages/strategy.rb} +41 -13
- data/lib/bard/plugins/github_pages.rb +30 -0
- data/lib/bard/plugins/hurt.rb +13 -0
- data/{install_files → lib/bard/plugins/install}/.github/dependabot.yml +2 -1
- data/{install_files → lib/bard/plugins/install}/.github/workflows/cache-ci.yml +1 -1
- data/{install_files → lib/bard/plugins/install}/.github/workflows/ci.yml +2 -2
- data/lib/bard/plugins/install.rb +9 -0
- data/lib/bard/plugins/open.rb +20 -0
- data/lib/bard/{ping.rb → plugins/ping/check.rb} +4 -4
- data/lib/bard/plugins/ping/target_methods.rb +23 -0
- data/lib/bard/plugins/ping.rb +10 -0
- data/lib/bard/plugins/run.rb +19 -0
- data/lib/bard/plugins/setup.rb +54 -0
- data/lib/bard/plugins/ssh/connection.rb +75 -0
- data/lib/bard/plugins/ssh/copy.rb +95 -0
- data/lib/bard/{ssh_server.rb → plugins/ssh/server.rb} +17 -42
- data/lib/bard/plugins/ssh/target_methods.rb +20 -0
- data/lib/bard/plugins/ssh.rb +10 -0
- data/lib/bard/plugins/url/target_methods.rb +23 -0
- data/lib/bard/plugins/url.rb +1 -0
- data/lib/bard/plugins/vim.rb +6 -0
- data/lib/bard/retryable.rb +25 -0
- data/lib/bard/secrets.rb +10 -0
- data/lib/bard/target.rb +27 -185
- data/lib/bard/version.rb +1 -1
- data/lib/bard.rb +1 -3
- data/spec/acceptance/docker/Dockerfile +3 -2
- data/spec/bard/capability_spec.rb +8 -50
- data/spec/bard/ci/github_actions_spec.rb +117 -14
- data/spec/bard/ci/jenkins_spec.rb +139 -0
- data/spec/bard/ci/runner_spec.rb +61 -0
- data/spec/bard/ci_spec.rb +1 -1
- data/spec/bard/cli/ci_spec.rb +34 -27
- data/spec/bard/cli/data_spec.rb +7 -26
- data/spec/bard/cli/deploy_spec.rb +87 -46
- data/spec/bard/cli/hurt_spec.rb +3 -9
- data/spec/bard/cli/install_spec.rb +5 -11
- data/spec/bard/cli/master_key_spec.rb +5 -19
- data/spec/bard/cli/open_spec.rb +14 -30
- data/spec/bard/cli/ping_spec.rb +8 -23
- data/spec/bard/cli/run_spec.rb +27 -21
- data/spec/bard/cli/setup_spec.rb +10 -27
- data/spec/bard/cli/ssh_spec.rb +10 -25
- data/spec/bard/cli/stage_spec.rb +28 -23
- data/spec/bard/cli/vim_spec.rb +3 -9
- data/spec/bard/command_spec.rb +1 -8
- data/spec/bard/config_spec.rb +78 -98
- data/spec/bard/copy_spec.rb +54 -18
- data/spec/bard/deploy_strategy/ssh_spec.rb +65 -7
- data/spec/bard/deploy_strategy_spec.rb +1 -1
- data/spec/bard/dynamic_dsl_spec.rb +18 -98
- data/spec/bard/git_spec.rb +9 -5
- data/spec/bard/github_spec.rb +2 -2
- data/spec/bard/ping_spec.rb +5 -5
- data/spec/bard/ssh_copy_spec.rb +44 -0
- data/spec/bard/ssh_server_spec.rb +8 -101
- data/spec/bard/target_spec.rb +66 -109
- data/spec/spec_helper.rb +6 -1
- metadata +79 -143
- data/README.rdoc +0 -15
- data/features/bard_check.feature +0 -94
- data/features/bard_deploy.feature +0 -18
- data/features/bard_pull.feature +0 -112
- data/features/bard_push.feature +0 -112
- data/features/podman_testcontainers.feature +0 -16
- data/features/step_definitions/check_steps.rb +0 -47
- data/features/step_definitions/git_steps.rb +0 -73
- data/features/step_definitions/global_steps.rb +0 -56
- data/features/step_definitions/podman_steps.rb +0 -23
- data/features/step_definitions/rails_steps.rb +0 -44
- data/features/step_definitions/submodule_steps.rb +0 -110
- data/features/support/grit_ext.rb +0 -13
- data/features/support/io.rb +0 -32
- data/features/support/podman.rb +0 -153
- data/lib/bard/ci/jenkins.rb +0 -105
- data/lib/bard/ci/retryable.rb +0 -27
- data/lib/bard/ci.rb +0 -50
- data/lib/bard/cli/ci.rb +0 -66
- data/lib/bard/cli/command.rb +0 -26
- data/lib/bard/cli/data.rb +0 -45
- data/lib/bard/cli/deploy.rb +0 -85
- data/lib/bard/cli/hurt.rb +0 -20
- data/lib/bard/cli/install.rb +0 -16
- data/lib/bard/cli/master_key.rb +0 -17
- data/lib/bard/cli/new.rb +0 -101
- data/lib/bard/cli/new_rails_template.rb +0 -197
- data/lib/bard/cli/open.rb +0 -22
- data/lib/bard/cli/ping.rb +0 -18
- data/lib/bard/cli/provision.rb +0 -34
- data/lib/bard/cli/run.rb +0 -24
- data/lib/bard/cli/setup.rb +0 -56
- data/lib/bard/cli/ssh.rb +0 -14
- data/lib/bard/cli/stage.rb +0 -27
- data/lib/bard/cli/vim.rb +0 -13
- data/lib/bard/default_config.rb +0 -35
- data/lib/bard/deploy_strategy/ssh.rb +0 -19
- data/lib/bard/github_pages.rb +0 -134
- data/lib/bard/provision/app.rb +0 -10
- data/lib/bard/provision/apt.rb +0 -16
- data/lib/bard/provision/authorizedkeys.rb +0 -25
- data/lib/bard/provision/data.rb +0 -27
- data/lib/bard/provision/deploy.rb +0 -10
- data/lib/bard/provision/http.rb +0 -16
- data/lib/bard/provision/logrotation.rb +0 -30
- data/lib/bard/provision/masterkey.rb +0 -18
- data/lib/bard/provision/mysql.rb +0 -22
- data/lib/bard/provision/passenger.rb +0 -37
- data/lib/bard/provision/repo.rb +0 -72
- data/lib/bard/provision/rvm.rb +0 -22
- data/lib/bard/provision/ssh.rb +0 -72
- data/lib/bard/provision/swapfile.rb +0 -21
- data/lib/bard/provision/user.rb +0 -42
- data/lib/bard/provision.rb +0 -16
- data/lib/bard/server.rb +0 -117
- data/spec/bard/cli/command_spec.rb +0 -50
- data/spec/bard/cli/new_spec.rb +0 -73
- data/spec/bard/cli/provision_spec.rb +0 -42
- data/spec/bard/github_pages_spec.rb +0 -143
- data/spec/bard/provision/app_spec.rb +0 -33
- data/spec/bard/provision/apt_spec.rb +0 -39
- data/spec/bard/provision/authorizedkeys_spec.rb +0 -40
- data/spec/bard/provision/data_spec.rb +0 -54
- data/spec/bard/provision/deploy_spec.rb +0 -33
- data/spec/bard/provision/http_spec.rb +0 -57
- data/spec/bard/provision/logrotation_spec.rb +0 -34
- data/spec/bard/provision/masterkey_spec.rb +0 -63
- data/spec/bard/provision/mysql_spec.rb +0 -55
- data/spec/bard/provision/passenger_spec.rb +0 -81
- data/spec/bard/provision/repo_spec.rb +0 -208
- data/spec/bard/provision/rvm_spec.rb +0 -49
- data/spec/bard/provision/ssh_spec.rb +0 -229
- data/spec/bard/provision/swapfile_spec.rb +0 -32
- data/spec/bard/provision/user_spec.rb +0 -103
- data/spec/bard/provision_spec.rb +0 -28
- data/spec/bard/server_spec.rb +0 -127
- /data/lib/bard/{ci → plugins/deploy/ci}/state.rb +0 -0
- /data/{install_files → lib/bard/plugins/install}/apt_dependencies.rb +0 -0
- /data/{install_files → lib/bard/plugins/install}/ci +0 -0
- /data/{install_files → lib/bard/plugins/install}/setup +0 -0
- /data/{install_files → lib/bard/plugins/install}/specified_bundler.rb +0 -0
- /data/{install_files → lib/bard/plugins/install}/specified_ruby.rb +0 -0
data/lib/bard/target.rb
CHANGED
|
@@ -1,22 +1,14 @@
|
|
|
1
|
-
require "uri"
|
|
2
1
|
require "bard/command"
|
|
3
|
-
require "bard/copy"
|
|
4
|
-
require "bard/deploy_strategy"
|
|
5
2
|
|
|
6
3
|
module Bard
|
|
7
4
|
class Target
|
|
8
|
-
attr_reader :key, :config
|
|
9
|
-
attr_accessor :server, :gateway, :ssh_key, :env
|
|
5
|
+
attr_reader :key, :config
|
|
10
6
|
|
|
11
7
|
def initialize(key, config)
|
|
12
8
|
@key = key
|
|
13
9
|
@config = config
|
|
14
10
|
@capabilities = []
|
|
15
|
-
@ping_urls = []
|
|
16
|
-
@strategy_options_hash = {}
|
|
17
|
-
@deploy_strategy = nil
|
|
18
11
|
@path = nil
|
|
19
|
-
@server = nil
|
|
20
12
|
end
|
|
21
13
|
|
|
22
14
|
# Capability tracking
|
|
@@ -30,193 +22,25 @@ module Bard
|
|
|
30
22
|
|
|
31
23
|
def require_capability!(capability)
|
|
32
24
|
unless has_capability?(capability)
|
|
33
|
-
|
|
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
|
|
25
|
+
raise "#{capability} capability not configured for this target"
|
|
170
26
|
end
|
|
171
27
|
end
|
|
172
28
|
|
|
173
|
-
def
|
|
174
|
-
|
|
29
|
+
def path
|
|
30
|
+
@path || config.project_name
|
|
175
31
|
end
|
|
176
32
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
Command.run!(command, on: server, home: home, verbose: verbose, quiet: quiet)
|
|
33
|
+
def run!(command, home: false, verbose: false, quiet: false, capture: false)
|
|
34
|
+
result = Command.run!(command, verbose:, quiet:)
|
|
35
|
+
result if capture
|
|
181
36
|
end
|
|
182
37
|
|
|
183
38
|
def run(command, home: false, verbose: false, quiet: false)
|
|
184
|
-
|
|
185
|
-
Command.run(command, on: server, home: home, verbose: verbose, quiet: quiet)
|
|
39
|
+
Command.run(command, verbose:, quiet:)
|
|
186
40
|
end
|
|
187
41
|
|
|
188
42
|
def exec!(command, home: false)
|
|
189
|
-
|
|
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
|
|
43
|
+
Command.exec!(command)
|
|
220
44
|
end
|
|
221
45
|
|
|
222
46
|
# Utility methods
|
|
@@ -235,5 +59,23 @@ module Bard
|
|
|
235
59
|
end
|
|
236
60
|
end
|
|
237
61
|
end
|
|
62
|
+
|
|
63
|
+
def ==(other)
|
|
64
|
+
return false unless other.is_a?(Bard::Target)
|
|
65
|
+
comparable_state == other.comparable_state
|
|
66
|
+
end
|
|
67
|
+
alias_method :eql?, :==
|
|
68
|
+
|
|
69
|
+
def hash
|
|
70
|
+
comparable_state.hash
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
protected
|
|
74
|
+
|
|
75
|
+
def comparable_state
|
|
76
|
+
(instance_variables - [:@key, :@config]).sort.map do |ivar|
|
|
77
|
+
[ivar, instance_variable_get(ivar)]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
238
80
|
end
|
|
239
81
|
end
|
data/lib/bard/version.rb
CHANGED
data/lib/bard.rb
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
FROM ubuntu:
|
|
1
|
+
FROM ubuntu:24.04
|
|
2
2
|
|
|
3
3
|
# Prevent interactive prompts
|
|
4
4
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
5
5
|
|
|
6
|
-
# Install SSH server and basic tools
|
|
6
|
+
# Install SSH server, git, and basic tools
|
|
7
7
|
RUN apt-get update && \
|
|
8
8
|
apt-get install -y \
|
|
9
9
|
openssh-server \
|
|
10
|
+
git \
|
|
10
11
|
sudo \
|
|
11
12
|
&& rm -rf /var/lib/apt/lists/*
|
|
12
13
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
require "spec_helper"
|
|
2
2
|
require "bard/target"
|
|
3
|
+
require "bard/plugins/ssh/target_methods"
|
|
4
|
+
require "bard/plugins/ping/target_methods"
|
|
3
5
|
|
|
4
6
|
describe "Capability System" do
|
|
5
7
|
let(:config) { double("config", project_name: "testapp") }
|
|
@@ -13,9 +15,9 @@ describe "Capability System" do
|
|
|
13
15
|
|
|
14
16
|
it "can enable multiple capabilities" do
|
|
15
17
|
target.enable_capability(:ssh)
|
|
16
|
-
target.enable_capability(:
|
|
18
|
+
target.enable_capability(:url)
|
|
17
19
|
expect(target.has_capability?(:ssh)).to be true
|
|
18
|
-
expect(target.has_capability?(:
|
|
20
|
+
expect(target.has_capability?(:url)).to be true
|
|
19
21
|
end
|
|
20
22
|
end
|
|
21
23
|
|
|
@@ -38,59 +40,15 @@ describe "Capability System" do
|
|
|
38
40
|
|
|
39
41
|
it "raises an error if capability is not enabled" do
|
|
40
42
|
expect { target.require_capability!(:ssh) }
|
|
41
|
-
.to raise_error(/
|
|
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/)
|
|
43
|
+
.to raise_error(/ssh capability not configured for this target/)
|
|
52
44
|
end
|
|
53
45
|
end
|
|
54
46
|
|
|
55
47
|
describe "capability dependency checking" do
|
|
56
|
-
context "
|
|
57
|
-
it "
|
|
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
|
|
48
|
+
context "URL-dependent methods" do
|
|
49
|
+
it "ping! requires url capability" do
|
|
87
50
|
expect { target.ping! }
|
|
88
|
-
.to raise_error(/
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
it "open requires ping capability" do
|
|
92
|
-
expect { target.open }
|
|
93
|
-
.to raise_error(/Ping URL not configured/)
|
|
51
|
+
.to raise_error(/url capability not configured/)
|
|
94
52
|
end
|
|
95
53
|
end
|
|
96
54
|
end
|
|
@@ -1,35 +1,138 @@
|
|
|
1
|
-
require "
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "bard/plugins/deploy/ci/github_actions"
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
BASE_URL = "https://api.github.com/repos/botandrosedesign/metrc"
|
|
5
|
+
|
|
6
|
+
RSpec.shared_context "github actions stubs" do
|
|
7
|
+
let(:run_id) { 123 }
|
|
8
|
+
let(:job_id) { 456 }
|
|
9
|
+
let(:started_at) { "2024-01-15T10:00:00Z" }
|
|
10
|
+
let(:completed_at) { "2024-01-15T10:01:30Z" }
|
|
11
|
+
let(:sha) { "abc123" }
|
|
12
|
+
|
|
13
|
+
let(:run_json) do
|
|
14
|
+
{
|
|
15
|
+
"id" => run_id,
|
|
16
|
+
"status" => "completed",
|
|
17
|
+
"conclusion" => "success",
|
|
18
|
+
"head_branch" => "master",
|
|
19
|
+
"head_sha" => sha,
|
|
20
|
+
"run_started_at" => started_at,
|
|
21
|
+
"updated_at" => completed_at,
|
|
22
|
+
}
|
|
23
|
+
end
|
|
5
24
|
|
|
6
|
-
|
|
7
|
-
|
|
25
|
+
let(:job_json) do
|
|
26
|
+
{
|
|
27
|
+
"id" => job_id,
|
|
28
|
+
"started_at" => started_at,
|
|
29
|
+
"completed_at" => completed_at,
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
before do
|
|
34
|
+
allow(Bard::Secrets).to receive(:fetch).with("github-apikey").and_return("test-key")
|
|
8
35
|
end
|
|
9
36
|
end
|
|
10
37
|
|
|
11
38
|
describe Bard::CI::GithubActions::API do
|
|
39
|
+
include_context "github actions stubs"
|
|
40
|
+
|
|
12
41
|
subject { described_class.new("metrc") }
|
|
13
42
|
|
|
14
43
|
describe "#last_successful_run" do
|
|
15
|
-
|
|
44
|
+
before do
|
|
45
|
+
stub_request(:get, "#{BASE_URL}/actions/runs")
|
|
46
|
+
.with(query: hash_including("status" => "success"))
|
|
47
|
+
.to_return(
|
|
48
|
+
headers: { "Content-Type" => "application/json" },
|
|
49
|
+
body: JSON.dump("workflow_runs" => [run_json]),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
stub_request(:get, "#{BASE_URL}/actions/runs/#{run_id}/jobs")
|
|
53
|
+
.with(query: hash_including("filter" => "latest"))
|
|
54
|
+
.to_return(
|
|
55
|
+
headers: { "Content-Type" => "application/json" },
|
|
56
|
+
body: JSON.dump("jobs" => [job_json]),
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "has #time_elapsed" do
|
|
16
61
|
run = subject.last_successful_run
|
|
17
|
-
run.time_elapsed
|
|
62
|
+
expect(run.time_elapsed).to eq 90
|
|
18
63
|
end
|
|
19
64
|
|
|
20
|
-
|
|
21
|
-
|
|
65
|
+
it "has #console" do
|
|
66
|
+
stub_request(:get, "#{BASE_URL}/actions/jobs/#{job_id}/logs")
|
|
67
|
+
.to_return(
|
|
68
|
+
headers: { "Content-Type" => "text/plain" },
|
|
69
|
+
body: "build log output here",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
expect(subject.last_successful_run.console).to eq "build log output here"
|
|
22
73
|
end
|
|
23
74
|
end
|
|
24
75
|
|
|
25
76
|
describe "#create_run!" do
|
|
26
|
-
|
|
27
|
-
|
|
77
|
+
it "returns a run" do
|
|
78
|
+
stub_request(:post, "#{BASE_URL}/actions/workflows/ci.yml/dispatches")
|
|
79
|
+
.to_return(status: 204, body: "")
|
|
80
|
+
|
|
81
|
+
allow(subject).to receive(:`).with("git rev-parse master").and_return("#{sha}\n")
|
|
82
|
+
|
|
83
|
+
stub_request(:get, "#{BASE_URL}/actions/runs")
|
|
84
|
+
.with(query: hash_including("head_sha" => sha))
|
|
85
|
+
.to_return(
|
|
86
|
+
headers: { "Content-Type" => "application/json" },
|
|
87
|
+
body: JSON.dump("workflow_runs" => [run_json]),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
run = subject.create_run!("master")
|
|
91
|
+
expect(run).to be_a Bard::CI::GithubActions::Run
|
|
92
|
+
expect(run.id).to eq run_id
|
|
28
93
|
end
|
|
29
94
|
end
|
|
30
95
|
end
|
|
31
96
|
|
|
32
|
-
describe Bard::
|
|
33
|
-
|
|
34
|
-
|
|
97
|
+
describe Bard::CI::GithubActions do
|
|
98
|
+
include_context "github actions stubs"
|
|
99
|
+
|
|
100
|
+
subject { described_class.new("metrc", "master", sha) }
|
|
101
|
+
|
|
102
|
+
it "returns true on successful run" do
|
|
103
|
+
stub_request(:post, "#{BASE_URL}/actions/workflows/ci.yml/dispatches")
|
|
104
|
+
.to_return(status: 204, body: "")
|
|
105
|
+
|
|
106
|
+
allow_any_instance_of(Bard::CI::GithubActions::API)
|
|
107
|
+
.to receive(:`).with("git rev-parse master").and_return("#{sha}\n")
|
|
35
108
|
|
|
109
|
+
stub_request(:get, "#{BASE_URL}/actions/runs")
|
|
110
|
+
.with(query: hash_including("head_sha" => sha))
|
|
111
|
+
.to_return(
|
|
112
|
+
headers: { "Content-Type" => "application/json" },
|
|
113
|
+
body: JSON.dump("workflow_runs" => [run_json]),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
stub_request(:get, "#{BASE_URL}/actions/runs")
|
|
117
|
+
.with(query: hash_including("status" => "success"))
|
|
118
|
+
.to_return(
|
|
119
|
+
headers: { "Content-Type" => "application/json" },
|
|
120
|
+
body: JSON.dump("workflow_runs" => [run_json]),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
stub_request(:get, "#{BASE_URL}/actions/runs/#{run_id}/jobs")
|
|
124
|
+
.with(query: hash_including("filter" => "latest"))
|
|
125
|
+
.to_return(
|
|
126
|
+
headers: { "Content-Type" => "application/json" },
|
|
127
|
+
body: JSON.dump("jobs" => [job_json]),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
stub_request(:get, "#{BASE_URL}/actions/runs/#{run_id}")
|
|
131
|
+
.to_return(
|
|
132
|
+
headers: { "Content-Type" => "application/json" },
|
|
133
|
+
body: JSON.dump(run_json),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
expect(subject.run { }).to eq true
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
require "bard/plugins/deploy/ci/jenkins"
|
|
3
|
+
|
|
4
|
+
RSpec.describe Bard::CI::Jenkins do
|
|
5
|
+
let(:jenkins) { described_class.new("test-project", "master", "abc123") }
|
|
6
|
+
|
|
7
|
+
before do
|
|
8
|
+
allow(Bard::Secrets).to receive(:fetch).with("jenkins-user").and_return("micah")
|
|
9
|
+
allow(Bard::Secrets).to receive(:fetch).with("jenkins-token").and_return("fake-token")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe "#get_last_time_elapsed" do
|
|
13
|
+
it "returns the duration in seconds from the last stable build" do
|
|
14
|
+
xml = "<build><duration>120000</duration></build>"
|
|
15
|
+
allow(jenkins).to receive(:`).with("curl -s http://micah:fake-token@ci.botandrose.com/job/test-project/lastStableBuild/api/xml").and_return(xml)
|
|
16
|
+
|
|
17
|
+
result = jenkins.send(:get_last_time_elapsed)
|
|
18
|
+
expect(result).to eq 120
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe "#run" do
|
|
23
|
+
let(:ci_url) { "http://micah:fake-token@ci.botandrose.com/job/test-project" }
|
|
24
|
+
|
|
25
|
+
before do
|
|
26
|
+
allow(jenkins).to receive(:sleep)
|
|
27
|
+
state = instance_double(Bard::CI::State, save: nil, delete: nil)
|
|
28
|
+
allow(jenkins).to receive(:state).and_return(state)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "waits until the build has started before polling" do
|
|
32
|
+
allow(jenkins).to receive(:`).with("curl -s -I -X POST -L '#{ci_url}/buildWithParameters?GIT_REF=master'").and_return("Location: http://ci.botandrose.com/queue/item/99/\r\n")
|
|
33
|
+
allow(jenkins).to receive(:`).with("curl -s #{ci_url}/lastStableBuild/api/xml").and_return("<build><duration>60000</duration></build>")
|
|
34
|
+
allow(jenkins).to receive(:`).with("curl -s -g '#{ci_url}/api/json?depth=1&tree=builds[queueId,number]'").and_return(
|
|
35
|
+
'{"builds":[{"queueId":1,"number":1}]}',
|
|
36
|
+
'{"builds":[{"queueId":99,"number":5}]}',
|
|
37
|
+
'{"builds":[{"queueId":99,"number":5}]}'
|
|
38
|
+
)
|
|
39
|
+
allow(jenkins).to receive(:`).with("curl -s #{ci_url}/5/api/json?tree=building,result").and_return('{"building":false,"result":"SUCCESS"}')
|
|
40
|
+
|
|
41
|
+
result = jenkins.run { |elapsed, last_time| }
|
|
42
|
+
expect(result).to eq true
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe "#started?" do
|
|
47
|
+
before do
|
|
48
|
+
jenkins.instance_variable_set(:@queueId, 99)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "retries when builds list is empty (job just created)" do
|
|
52
|
+
allow(jenkins).to receive(:`).with(/api\/json\?depth=1/).and_return(
|
|
53
|
+
'{"builds":[]}',
|
|
54
|
+
'{"builds":[{"queueId":99,"number":1}]}'
|
|
55
|
+
)
|
|
56
|
+
allow(jenkins).to receive(:sleep)
|
|
57
|
+
|
|
58
|
+
expect(jenkins.send(:started?)).to eq true
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe "#exists?" do
|
|
63
|
+
it "returns truthy when the job exists" do
|
|
64
|
+
allow(jenkins).to receive(:`).with(/curl -s -I/).and_return("HTTP/1.1 200 OK\r\n")
|
|
65
|
+
|
|
66
|
+
expect(jenkins.exists?).to be_truthy
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "auto-creates the job when it does not exist" do
|
|
70
|
+
allow(jenkins).to receive(:`).with(/curl -s -I/).and_return("HTTP/1.1 404 Not Found\r\n")
|
|
71
|
+
allow(jenkins).to receive(:`).with("git remote get-url origin").and_return("git@gitlab.com:botandrose/test-project.git\n")
|
|
72
|
+
allow(File).to receive(:exist?).with("config/master.key").and_return(false)
|
|
73
|
+
expect(jenkins).to receive(:`).with(/curl -s -X POST.*createItem\?name=test-project.*Content-Type: application\/xml/)
|
|
74
|
+
|
|
75
|
+
jenkins.exists?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "includes a master key build step when config/master.key exists" do
|
|
79
|
+
allow(jenkins).to receive(:`).with(/curl -s -I/).and_return("HTTP/1.1 404 Not Found\r\n")
|
|
80
|
+
allow(jenkins).to receive(:`).with("git remote get-url origin").and_return("git@gitlab.com:botandrose/test-project.git\n")
|
|
81
|
+
allow(File).to receive(:exist?).with("config/master.key").and_return(true)
|
|
82
|
+
allow(File).to receive(:read).with("config/master.key").and_return("abc123secret")
|
|
83
|
+
|
|
84
|
+
config_xml = nil
|
|
85
|
+
allow(jenkins).to receive(:`).with(/createItem/) do |cmd|
|
|
86
|
+
config_xml = cmd
|
|
87
|
+
""
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
jenkins.exists?
|
|
91
|
+
expect(config_xml).to include("abc123secret")
|
|
92
|
+
expect(config_xml).to include("config/master.key")
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
describe "#building? and #success?" do
|
|
97
|
+
before do
|
|
98
|
+
jenkins.instance_variable_set(:@job_id, 42)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "detects a successful build" do
|
|
102
|
+
allow(jenkins).to receive(:`).with("curl -s http://micah:fake-token@ci.botandrose.com/job/test-project/42/api/json?tree=building,result").and_return('{"building":false,"result":"SUCCESS"}')
|
|
103
|
+
|
|
104
|
+
expect(jenkins.send(:building?)).to eq false
|
|
105
|
+
expect(jenkins.send(:success?)).to eq true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "detects a failed build" do
|
|
109
|
+
allow(jenkins).to receive(:`).with("curl -s http://micah:fake-token@ci.botandrose.com/job/test-project/42/api/json?tree=building,result").and_return('{"building":false,"result":"FAILURE"}')
|
|
110
|
+
|
|
111
|
+
expect(jenkins.send(:building?)).to eq false
|
|
112
|
+
expect(jenkins.send(:success?)).to eq false
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "detects a build in progress" do
|
|
116
|
+
allow(jenkins).to receive(:`).with("curl -s http://micah:fake-token@ci.botandrose.com/job/test-project/42/api/json?tree=building,result").and_return('{"building":true,"result":null}')
|
|
117
|
+
|
|
118
|
+
expect(jenkins.send(:building?)).to eq true
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "handles JSON with spaces in keys" do
|
|
122
|
+
allow(jenkins).to receive(:`).with("curl -s http://micah:fake-token@ci.botandrose.com/job/test-project/42/api/json?tree=building,result").and_return('{"_class":"hudson.model.FreeStyleBuild","building":false,"result":"SUCCESS"}')
|
|
123
|
+
|
|
124
|
+
expect(jenkins.send(:building?)).to eq false
|
|
125
|
+
expect(jenkins.send(:success?)).to eq true
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "success? reflects the last response from building?" do
|
|
129
|
+
allow(jenkins).to receive(:`).with("curl -s http://micah:fake-token@ci.botandrose.com/job/test-project/42/api/json?tree=building,result").and_return(
|
|
130
|
+
'{"building":true,"result":null}',
|
|
131
|
+
'{"building":false,"result":"SUCCESS"}'
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
jenkins.send(:building?) # first call — still building
|
|
135
|
+
jenkins.send(:building?) # second call — done
|
|
136
|
+
expect(jenkins.send(:success?)).to eq true
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|