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.
Files changed (174) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +6 -1
  3. data/CLAUDE.md +76 -0
  4. data/MIGRATION_GUIDE.md +24 -9
  5. data/PLUGINS.md +99 -0
  6. data/README.md +14 -6
  7. data/Rakefile +3 -1
  8. data/bard.gemspec +2 -1
  9. data/cucumber.yml +1 -0
  10. data/features/ci.feature +63 -0
  11. data/features/data.feature +13 -0
  12. data/features/deploy.feature +14 -0
  13. data/features/deploy_git_workflow.feature +89 -0
  14. data/features/run.feature +14 -0
  15. data/features/step_definitions/bard_steps.rb +136 -0
  16. data/features/support/bard-coverage +16 -0
  17. data/features/support/env.rb +14 -39
  18. data/features/support/test_server.rb +216 -0
  19. data/lib/bard/cli.rb +14 -31
  20. data/lib/bard/command.rb +10 -69
  21. data/lib/bard/config.rb +40 -183
  22. data/lib/bard/copy.rb +28 -103
  23. data/lib/bard/plugins/data.rb +56 -0
  24. data/lib/bard/{ci → plugins/deploy/ci}/github_actions.rb +3 -4
  25. data/lib/bard/plugins/deploy/ci/jenkins.rb +176 -0
  26. data/lib/bard/{ci → plugins/deploy/ci}/local.rb +7 -7
  27. data/lib/bard/{ci → plugins/deploy/ci}/runner.rb +38 -4
  28. data/lib/bard/plugins/deploy/ci.rb +38 -0
  29. data/lib/bard/plugins/deploy/ssh_strategy.rb +27 -0
  30. data/lib/bard/{deploy_strategy.rb → plugins/deploy/strategy.rb} +1 -1
  31. data/lib/bard/plugins/deploy.rb +240 -0
  32. data/lib/bard/{git.rb → plugins/git.rb} +6 -3
  33. data/lib/bard/{github.rb → plugins/github.rb} +4 -6
  34. data/lib/bard/{deploy_strategy/github_pages.rb → plugins/github_pages/strategy.rb} +41 -13
  35. data/lib/bard/plugins/github_pages.rb +30 -0
  36. data/lib/bard/plugins/hurt.rb +13 -0
  37. data/{install_files → lib/bard/plugins/install}/.github/dependabot.yml +2 -1
  38. data/{install_files → lib/bard/plugins/install}/.github/workflows/cache-ci.yml +1 -1
  39. data/{install_files → lib/bard/plugins/install}/.github/workflows/ci.yml +2 -2
  40. data/lib/bard/plugins/install.rb +9 -0
  41. data/lib/bard/plugins/open.rb +20 -0
  42. data/lib/bard/{ping.rb → plugins/ping/check.rb} +4 -4
  43. data/lib/bard/plugins/ping/target_methods.rb +23 -0
  44. data/lib/bard/plugins/ping.rb +10 -0
  45. data/lib/bard/plugins/run.rb +19 -0
  46. data/lib/bard/plugins/setup.rb +54 -0
  47. data/lib/bard/plugins/ssh/connection.rb +75 -0
  48. data/lib/bard/plugins/ssh/copy.rb +95 -0
  49. data/lib/bard/{ssh_server.rb → plugins/ssh/server.rb} +17 -42
  50. data/lib/bard/plugins/ssh/target_methods.rb +20 -0
  51. data/lib/bard/plugins/ssh.rb +10 -0
  52. data/lib/bard/plugins/url/target_methods.rb +23 -0
  53. data/lib/bard/plugins/url.rb +1 -0
  54. data/lib/bard/plugins/vim.rb +6 -0
  55. data/lib/bard/retryable.rb +25 -0
  56. data/lib/bard/secrets.rb +10 -0
  57. data/lib/bard/target.rb +27 -185
  58. data/lib/bard/version.rb +1 -1
  59. data/lib/bard.rb +1 -3
  60. data/spec/acceptance/docker/Dockerfile +3 -2
  61. data/spec/bard/capability_spec.rb +8 -50
  62. data/spec/bard/ci/github_actions_spec.rb +117 -14
  63. data/spec/bard/ci/jenkins_spec.rb +139 -0
  64. data/spec/bard/ci/runner_spec.rb +61 -0
  65. data/spec/bard/ci_spec.rb +1 -1
  66. data/spec/bard/cli/ci_spec.rb +34 -27
  67. data/spec/bard/cli/data_spec.rb +7 -26
  68. data/spec/bard/cli/deploy_spec.rb +87 -46
  69. data/spec/bard/cli/hurt_spec.rb +3 -9
  70. data/spec/bard/cli/install_spec.rb +5 -11
  71. data/spec/bard/cli/master_key_spec.rb +5 -19
  72. data/spec/bard/cli/open_spec.rb +14 -30
  73. data/spec/bard/cli/ping_spec.rb +8 -23
  74. data/spec/bard/cli/run_spec.rb +27 -21
  75. data/spec/bard/cli/setup_spec.rb +10 -27
  76. data/spec/bard/cli/ssh_spec.rb +10 -25
  77. data/spec/bard/cli/stage_spec.rb +28 -23
  78. data/spec/bard/cli/vim_spec.rb +3 -9
  79. data/spec/bard/command_spec.rb +1 -8
  80. data/spec/bard/config_spec.rb +78 -98
  81. data/spec/bard/copy_spec.rb +54 -18
  82. data/spec/bard/deploy_strategy/ssh_spec.rb +65 -7
  83. data/spec/bard/deploy_strategy_spec.rb +1 -1
  84. data/spec/bard/dynamic_dsl_spec.rb +18 -98
  85. data/spec/bard/git_spec.rb +9 -5
  86. data/spec/bard/github_spec.rb +2 -2
  87. data/spec/bard/ping_spec.rb +5 -5
  88. data/spec/bard/ssh_copy_spec.rb +44 -0
  89. data/spec/bard/ssh_server_spec.rb +8 -101
  90. data/spec/bard/target_spec.rb +66 -109
  91. data/spec/spec_helper.rb +6 -1
  92. metadata +79 -143
  93. data/README.rdoc +0 -15
  94. data/features/bard_check.feature +0 -94
  95. data/features/bard_deploy.feature +0 -18
  96. data/features/bard_pull.feature +0 -112
  97. data/features/bard_push.feature +0 -112
  98. data/features/podman_testcontainers.feature +0 -16
  99. data/features/step_definitions/check_steps.rb +0 -47
  100. data/features/step_definitions/git_steps.rb +0 -73
  101. data/features/step_definitions/global_steps.rb +0 -56
  102. data/features/step_definitions/podman_steps.rb +0 -23
  103. data/features/step_definitions/rails_steps.rb +0 -44
  104. data/features/step_definitions/submodule_steps.rb +0 -110
  105. data/features/support/grit_ext.rb +0 -13
  106. data/features/support/io.rb +0 -32
  107. data/features/support/podman.rb +0 -153
  108. data/lib/bard/ci/jenkins.rb +0 -105
  109. data/lib/bard/ci/retryable.rb +0 -27
  110. data/lib/bard/ci.rb +0 -50
  111. data/lib/bard/cli/ci.rb +0 -66
  112. data/lib/bard/cli/command.rb +0 -26
  113. data/lib/bard/cli/data.rb +0 -45
  114. data/lib/bard/cli/deploy.rb +0 -85
  115. data/lib/bard/cli/hurt.rb +0 -20
  116. data/lib/bard/cli/install.rb +0 -16
  117. data/lib/bard/cli/master_key.rb +0 -17
  118. data/lib/bard/cli/new.rb +0 -101
  119. data/lib/bard/cli/new_rails_template.rb +0 -197
  120. data/lib/bard/cli/open.rb +0 -22
  121. data/lib/bard/cli/ping.rb +0 -18
  122. data/lib/bard/cli/provision.rb +0 -34
  123. data/lib/bard/cli/run.rb +0 -24
  124. data/lib/bard/cli/setup.rb +0 -56
  125. data/lib/bard/cli/ssh.rb +0 -14
  126. data/lib/bard/cli/stage.rb +0 -27
  127. data/lib/bard/cli/vim.rb +0 -13
  128. data/lib/bard/default_config.rb +0 -35
  129. data/lib/bard/deploy_strategy/ssh.rb +0 -19
  130. data/lib/bard/github_pages.rb +0 -134
  131. data/lib/bard/provision/app.rb +0 -10
  132. data/lib/bard/provision/apt.rb +0 -16
  133. data/lib/bard/provision/authorizedkeys.rb +0 -25
  134. data/lib/bard/provision/data.rb +0 -27
  135. data/lib/bard/provision/deploy.rb +0 -10
  136. data/lib/bard/provision/http.rb +0 -16
  137. data/lib/bard/provision/logrotation.rb +0 -30
  138. data/lib/bard/provision/masterkey.rb +0 -18
  139. data/lib/bard/provision/mysql.rb +0 -22
  140. data/lib/bard/provision/passenger.rb +0 -37
  141. data/lib/bard/provision/repo.rb +0 -72
  142. data/lib/bard/provision/rvm.rb +0 -22
  143. data/lib/bard/provision/ssh.rb +0 -72
  144. data/lib/bard/provision/swapfile.rb +0 -21
  145. data/lib/bard/provision/user.rb +0 -42
  146. data/lib/bard/provision.rb +0 -16
  147. data/lib/bard/server.rb +0 -117
  148. data/spec/bard/cli/command_spec.rb +0 -50
  149. data/spec/bard/cli/new_spec.rb +0 -73
  150. data/spec/bard/cli/provision_spec.rb +0 -42
  151. data/spec/bard/github_pages_spec.rb +0 -143
  152. data/spec/bard/provision/app_spec.rb +0 -33
  153. data/spec/bard/provision/apt_spec.rb +0 -39
  154. data/spec/bard/provision/authorizedkeys_spec.rb +0 -40
  155. data/spec/bard/provision/data_spec.rb +0 -54
  156. data/spec/bard/provision/deploy_spec.rb +0 -33
  157. data/spec/bard/provision/http_spec.rb +0 -57
  158. data/spec/bard/provision/logrotation_spec.rb +0 -34
  159. data/spec/bard/provision/masterkey_spec.rb +0 -63
  160. data/spec/bard/provision/mysql_spec.rb +0 -55
  161. data/spec/bard/provision/passenger_spec.rb +0 -81
  162. data/spec/bard/provision/repo_spec.rb +0 -208
  163. data/spec/bard/provision/rvm_spec.rb +0 -49
  164. data/spec/bard/provision/ssh_spec.rb +0 -229
  165. data/spec/bard/provision/swapfile_spec.rb +0 -32
  166. data/spec/bard/provision/user_spec.rb +0 -103
  167. data/spec/bard/provision_spec.rb +0 -28
  168. data/spec/bard/server_spec.rb +0 -127
  169. /data/lib/bard/{ci → plugins/deploy/ci}/state.rb +0 -0
  170. /data/{install_files → lib/bard/plugins/install}/apt_dependencies.rb +0 -0
  171. /data/{install_files → lib/bard/plugins/install}/ci +0 -0
  172. /data/{install_files → lib/bard/plugins/install}/setup +0 -0
  173. /data/{install_files → lib/bard/plugins/install}/specified_bundler.rb +0 -0
  174. /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, :path
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
- 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
25
+ raise "#{capability} capability not configured for this target"
170
26
  end
171
27
  end
172
28
 
173
- def respond_to_missing?(method, include_private = false)
174
- DeployStrategy[method] || super
29
+ def path
30
+ @path || config.project_name
175
31
  end
176
32
 
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)
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
- require_capability!(:ssh)
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
- 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
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
@@ -1,4 +1,4 @@
1
1
  module Bard
2
- VERSION = "2.0.0.beta"
2
+ VERSION = "2.0.1"
3
3
  end
4
4
 
data/lib/bard.rb CHANGED
@@ -1,3 +1 @@
1
- module Bard
2
- end
3
-
1
+ require "bard/cli"
@@ -1,12 +1,13 @@
1
- FROM ubuntu:22.04
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(:ping)
18
+ target.enable_capability(:url)
17
19
  expect(target.has_capability?(:ssh)).to be true
18
- expect(target.has_capability?(:ping)).to be true
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(/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/)
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 "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
48
+ context "URL-dependent methods" do
49
+ it "ping! requires url capability" do
87
50
  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/)
51
+ .to raise_error(/url capability not configured/)
94
52
  end
95
53
  end
96
54
  end
@@ -1,35 +1,138 @@
1
- require "bard/ci/github_actions"
1
+ require "spec_helper"
2
+ require "bard/plugins/deploy/ci/github_actions"
2
3
 
3
- describe Bard::CI::GithubActions do
4
- subject { described_class.new("metrc", "master", "0966308e204b256fdcc11457eb53306d84884c60") }
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
- xit "works" do
7
- subject.run
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
- xit "has #time_elapsed" do
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
- xit "has #console" do
21
- subject.last_successful_run.console
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
- xit "returns a run" do
27
- subject.create_run! "master"
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::Github do
33
- subject { described_class.new("metrc") }
34
- end
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