bard 2.0.0.beta → 2.0.0

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} +13 -6
  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
@@ -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
@@ -0,0 +1,61 @@
1
+ require "bard/plugins/deploy/ci/runner"
2
+
3
+ RSpec.describe Bard::CI::Runner do
4
+ describe ".runners" do
5
+ it "is a hash registry" do
6
+ expect(described_class.runners).to be_a(Hash)
7
+ end
8
+ end
9
+
10
+ describe ".[]" do
11
+ before do
12
+ require "bard/plugins/deploy/ci/local"
13
+ require "bard/plugins/deploy/ci/github_actions"
14
+ end
15
+
16
+ it "looks up runners by name" do
17
+ expect(described_class[:local]).to eq Bard::CI::Local
18
+ expect(described_class[:github_actions]).to eq Bard::CI::GithubActions
19
+ end
20
+
21
+ it "returns nil for unknown runners" do
22
+ expect(described_class[:nonexistent]).to be_nil
23
+ end
24
+ end
25
+
26
+ describe ".default" do
27
+ it "returns the last registered runner" do
28
+ # Whatever was registered last in the current test run
29
+ expect(described_class.default).to be_a(Class)
30
+ expect(described_class.default.ancestors).to include(Bard::CI::Runner)
31
+ end
32
+ end
33
+
34
+ describe "auto-registration via inherited" do
35
+ it "registers subclasses automatically" do
36
+ eval <<-RUBY
37
+ module Bard
38
+ class CI
39
+ class SpecTestRunner < Runner
40
+ end
41
+ end
42
+ end
43
+ RUBY
44
+
45
+ expect(described_class[:spec_test_runner]).to eq Bard::CI::SpecTestRunner
46
+ end
47
+
48
+ it "newly registered runner becomes the default" do
49
+ eval <<-RUBY
50
+ module Bard
51
+ class CI
52
+ class AnotherTestRunner < Runner
53
+ end
54
+ end
55
+ end
56
+ RUBY
57
+
58
+ expect(described_class.default).to eq Bard::CI::AnotherTestRunner
59
+ end
60
+ end
61
+ end
data/spec/bard/ci_spec.rb CHANGED
@@ -1,4 +1,4 @@
1
- require "bard/ci"
1
+ require "bard/plugins/deploy/ci"
2
2
 
3
3
  describe Bard::CI do
4
4
  subject { described_class.new("tracker", "master") }
@@ -1,28 +1,14 @@
1
1
  require "spec_helper"
2
2
  require "bard/cli"
3
- require "bard/cli/ci"
4
- require "thor"
3
+ require "ostruct"
5
4
 
6
- class TestCICLI < Thor
7
- include Bard::CLI::CI
8
-
9
- attr_reader :options
10
-
11
- def initialize
12
- super
13
- @options = {}
14
- end
15
-
16
- def project_name
17
- "test_project"
18
- end
19
- end
20
-
21
- describe Bard::CLI::CI do
22
- let(:cli) { TestCICLI.new }
5
+ describe "bard ci" do
6
+ let(:cli) { Bard::CLI.new }
23
7
  let(:ci_runner) { double("ci_runner") }
24
8
 
25
9
  before do
10
+ allow(cli).to receive(:config).and_return(OpenStruct.new(ci: nil))
11
+ allow(cli).to receive(:project_name).and_return("test_project")
26
12
  allow(cli).to receive(:puts)
27
13
  allow(cli).to receive(:print)
28
14
  allow(cli).to receive(:exit)
@@ -101,12 +87,9 @@ describe Bard::CLI::CI do
101
87
  it "shows error message and exits" do
102
88
  allow(cli).to receive(:options).and_return({})
103
89
  allow(ci_runner).to receive(:exists?).and_return(false)
90
+ allow(cli).to receive(:exit).with(1).and_raise(SystemExit)
104
91
 
105
- expect(cli).to receive(:puts) # "No CI found for test_project!"
106
- expect(cli).to receive(:puts) # "Re-run with --skip-ci to bypass CI..."
107
- expect(cli).to receive(:exit).with(1)
108
-
109
- cli.ci
92
+ expect { cli.ci }.to raise_error(SystemExit)
110
93
  end
111
94
  end
112
95
 
@@ -116,7 +99,7 @@ describe Bard::CLI::CI do
116
99
  allow(ci_runner).to receive(:exists?).and_return(true)
117
100
  allow(ci_runner).to receive(:run).and_return(true)
118
101
 
119
- expect(Bard::CI).to receive(:new).with("test_project", "develop", local: nil)
102
+ expect(Bard::CI).to receive(:new).with("test_project", "develop", runner_name: nil)
120
103
  expect(cli).to receive(:puts).with("Continuous integration: starting build on develop...")
121
104
 
122
105
  cli.ci("develop")
@@ -124,12 +107,36 @@ describe Bard::CLI::CI do
124
107
  end
125
108
 
126
109
  context "with local-ci option" do
127
- it "passes local option to CI runner" do
110
+ it "passes local runner_name to CI" do
128
111
  allow(cli).to receive(:options).and_return({ "local-ci" => true })
129
112
  allow(ci_runner).to receive(:exists?).and_return(true)
130
113
  allow(ci_runner).to receive(:run).and_return(true)
131
114
 
132
- expect(Bard::CI).to receive(:new).with("test_project", "feature-branch", local: true)
115
+ expect(Bard::CI).to receive(:new).with("test_project", "feature-branch", runner_name: :local)
116
+
117
+ cli.ci
118
+ end
119
+ end
120
+
121
+ context "with ci option" do
122
+ it "passes specified runner_name to CI" do
123
+ allow(cli).to receive(:options).and_return({ "ci" => "jenkins" })
124
+ allow(ci_runner).to receive(:exists?).and_return(true)
125
+ allow(ci_runner).to receive(:run).and_return(true)
126
+
127
+ expect(Bard::CI).to receive(:new).with("test_project", "feature-branch", runner_name: :jenkins)
128
+
129
+ cli.ci
130
+ end
131
+ end
132
+
133
+ context "with both local-ci and ci options" do
134
+ it "local-ci takes precedence" do
135
+ allow(cli).to receive(:options).and_return({ "local-ci" => true, "ci" => "jenkins" })
136
+ allow(ci_runner).to receive(:exists?).and_return(true)
137
+ allow(ci_runner).to receive(:run).and_return(true)
138
+
139
+ expect(Bard::CI).to receive(:new).with("test_project", "feature-branch", runner_name: :local)
133
140
 
134
141
  cli.ci
135
142
  end
@@ -1,35 +1,15 @@
1
1
  require "spec_helper"
2
2
  require "bard/cli"
3
- require "bard/cli/data"
4
3
 
5
- require "thor"
6
-
7
- require "term/ansicolor"
8
-
9
- class TestCLI < Thor
10
- include Bard::CLI::Data
11
- include Term::ANSIColor
12
-
13
- attr_reader :config
14
-
15
- def initialize
16
- @config = {}
17
- end
18
-
19
- def options
20
- {}
21
- end
22
- end
23
-
24
- describe Bard::CLI::Data do
25
- let(:cli) { TestCLI.new }
4
+ describe "bard data" do
5
+ let(:cli) { Bard::CLI.new }
26
6
 
27
7
  it "should have a data command" do
28
8
  expect(cli).to respond_to(:data)
29
9
  end
30
10
 
31
11
  context "data" do
32
- let(:from) { double("from", key: :production, run!: nil, copy_file: nil, copy_dir: nil) }
12
+ let(:from) { double("from", key: :production, run!: nil, require_capability!: nil) }
33
13
  let(:to) { double("to", key: :local, run!: nil) }
34
14
 
35
15
  let(:config) do
@@ -42,17 +22,18 @@ describe Bard::CLI::Data do
42
22
  before do
43
23
  allow(cli).to receive(:config).and_return(config)
44
24
  allow(cli).to receive(:options).and_return({from: "production", to: "local"})
25
+ allow(cli).to receive(:puts)
45
26
  end
46
27
 
47
28
  it "should run the data command" do
48
29
  expect(from).to receive(:run!).with("bin/rake db:dump")
49
- expect(from).to receive(:copy_file).with("db/data.sql.gz", to: to, verbose: true)
30
+ expect(Bard::Copy).to receive(:file).with("db/data.sql.gz", from: from, to: to, verbose: true)
50
31
  expect(to).to receive(:run!).with("bin/rake db:load")
51
32
  cli.data
52
33
  end
53
34
 
54
35
  context "pushing to production" do
55
- let(:to) { double("to", key: :production, ping: ["https://example.com"]) }
36
+ let(:to) { double("to", key: :production, url: "https://example.com", run!: nil, require_capability!: nil) }
56
37
 
57
38
  before do
58
39
  allow(cli).to receive(:options).and_return({from: "local", to: "production"})
@@ -68,7 +49,7 @@ describe Bard::CLI::Data do
68
49
  it "should allow pushing to production if the user confirms" do
69
50
  expect(cli).to receive(:ask).and_return("https://example.com")
70
51
  expect(from).to receive(:run!).with("bin/rake db:dump")
71
- expect(from).to receive(:copy_file).with("db/data.sql.gz", to: to, verbose: true)
52
+ expect(Bard::Copy).to receive(:file).with("db/data.sql.gz", from: from, to: to, verbose: true)
72
53
  expect(to).to receive(:run!).with("bin/rake db:load")
73
54
  cli.data
74
55
  end