bard 1.8.0 → 1.9.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +76 -0
  3. data/PLUGINS.md +114 -0
  4. data/README.md +14 -6
  5. data/features/ci.feature +62 -0
  6. data/features/deploy_git_workflow.feature +88 -0
  7. data/features/step_definitions/bard_steps.rb +96 -0
  8. data/features/support/bard-coverage +16 -0
  9. data/features/support/env.rb +10 -1
  10. data/features/support/test_server.rb +2 -1
  11. data/lib/bard/ci/github_actions.rb +1 -2
  12. data/lib/bard/ci/jenkins.rb +82 -11
  13. data/lib/bard/ci/local.rb +6 -6
  14. data/lib/bard/ci/runner.rb +35 -1
  15. data/lib/bard/ci.rb +11 -23
  16. data/lib/bard/cli/ci.rb +45 -38
  17. data/lib/bard/cli/deploy.rb +40 -8
  18. data/lib/bard/cli/hurt.rb +10 -15
  19. data/lib/bard/cli/install.rb +7 -12
  20. data/lib/bard/cli/open.rb +12 -16
  21. data/lib/bard/cli/ping.rb +8 -14
  22. data/lib/bard/cli/run.rb +5 -3
  23. data/lib/bard/cli/vim.rb +5 -10
  24. data/lib/bard/cli.rb +7 -12
  25. data/lib/bard/config.rb +1 -13
  26. data/lib/bard/github.rb +2 -4
  27. data/lib/bard/plugin.rb +99 -0
  28. data/lib/bard/plugins/backup.rb +19 -0
  29. data/lib/bard/plugins/github_pages.rb +34 -0
  30. data/lib/bard/plugins/hurt.rb +5 -0
  31. data/lib/bard/plugins/install.rb +5 -0
  32. data/lib/bard/plugins/jenkins.rb +6 -0
  33. data/lib/bard/plugins/new.rb +5 -0
  34. data/lib/bard/plugins/ping.rb +6 -0
  35. data/lib/bard/plugins/provision.rb +5 -0
  36. data/lib/bard/plugins/vim.rb +5 -0
  37. data/lib/bard/secrets.rb +10 -0
  38. data/lib/bard/version.rb +1 -1
  39. data/spec/bard/ci/github_actions_spec.rb +116 -13
  40. data/spec/bard/ci/jenkins_spec.rb +139 -0
  41. data/spec/bard/ci/runner_spec.rb +61 -0
  42. data/spec/bard/cli/ci_spec.rb +34 -8
  43. data/spec/bard/cli/deploy_spec.rb +20 -8
  44. data/spec/bard/cli/hurt_spec.rb +2 -2
  45. data/spec/bard/cli/install_spec.rb +4 -4
  46. data/spec/bard/cli/open_spec.rb +10 -8
  47. data/spec/bard/cli/ping_spec.rb +5 -5
  48. data/spec/bard/cli/run_spec.rb +20 -1
  49. data/spec/bard/cli/vim_spec.rb +5 -5
  50. data/spec/bard/github_spec.rb +1 -1
  51. data/spec/bard/plugin_spec.rb +79 -0
  52. data/spec/spec_helper.rb +6 -1
  53. metadata +27 -3
  54. data/README.rdoc +0 -15
data/lib/bard/cli.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require "thor"
4
4
  require "bard/config"
5
5
  require "bard/command"
6
+ require "bard/plugin"
6
7
  require "term/ansicolor"
7
8
 
8
9
  module Bard
@@ -19,24 +20,18 @@ module Bard
19
20
  master_key: "MasterKey",
20
21
  setup: "Setup",
21
22
  run: "Run",
22
- open: "Open",
23
23
  ssh: "SSH",
24
- install: "Install",
25
- ping: "Ping",
26
- hurt: "Hurt",
27
- vim: "Vim",
28
24
  }.each do |command, klass|
29
25
  require "bard/cli/#{command}"
30
26
  include const_get(klass)
31
27
  end
32
28
 
33
- {
34
- provision: "Provision",
35
- new: "New",
36
- }.each do |command, klass|
37
- require "bard/cli/#{command}"
38
- const_get(klass).setup(self)
39
- end
29
+ Plugin.load_all!
30
+ Plugin.all.each { |plugin| plugin.apply_to_cli(self) }
31
+
32
+ # Load core CI runners AFTER plugins so GithubActions is the default (last registered wins)
33
+ require "bard/ci/local"
34
+ require "bard/ci/github_actions"
40
35
 
41
36
  def self.exit_on_failure? = true
42
37
 
data/lib/bard/config.rb CHANGED
@@ -131,19 +131,7 @@ module Bard
131
131
  return nil if @ci_system == false
132
132
 
133
133
  require "bard/ci"
134
-
135
- # Use the existing CI class which handles auto-detection
136
- case @ci_system
137
- when :local
138
- CI.new(project_name, branch, local: true)
139
- when :github_actions, :jenkins, nil
140
- # CI class auto-detects between github_actions and jenkins
141
- CI.new(project_name, branch)
142
- when false
143
- nil
144
- else
145
- CI.new(project_name, branch)
146
- end
134
+ CI.new(project_name, branch, runner_name: @ci_system)
147
135
  end
148
136
 
149
137
  private
data/lib/bard/github.rb CHANGED
@@ -3,6 +3,7 @@ require "json"
3
3
  require "base64"
4
4
  require "rbnacl"
5
5
  require "bard/ci/retryable"
6
+ require "bard/secrets"
6
7
 
7
8
  module Bard
8
9
  class Github < Struct.new(:project_name)
@@ -107,10 +108,7 @@ module Bard
107
108
  private
108
109
 
109
110
  def api_key
110
- @api_key ||= begin
111
- raw = `git ls-remote -t git@github.com:botandrosedesign/secrets`
112
- raw[/github-apikey\|(.+)$/, 1]
113
- end
111
+ @api_key ||= Bard::Secrets.fetch("github-apikey")
114
112
  end
115
113
 
116
114
  def request path, &block
@@ -0,0 +1,99 @@
1
+ module Bard
2
+ class Plugin
3
+ @registry = {}
4
+
5
+ class << self
6
+ attr_reader :registry
7
+
8
+ def register(name, &block)
9
+ plugin = new(name)
10
+ plugin.instance_eval(&block) if block
11
+ @registry[name.to_sym] = plugin
12
+ end
13
+
14
+ def [](name)
15
+ @registry[name.to_sym]
16
+ end
17
+
18
+ def all
19
+ @registry.values
20
+ end
21
+
22
+ def load_all!
23
+ Dir[File.join(__dir__, "plugins", "*.rb")].sort.each { |f| require f }
24
+ all.each(&:apply!)
25
+ end
26
+
27
+ def reset!
28
+ @registry = {}
29
+ end
30
+ end
31
+
32
+ attr_reader :name, :cli_modules
33
+
34
+ def initialize(name)
35
+ @name = name.to_sym
36
+ @cli_modules = []
37
+ @cli_requires = []
38
+ @target_methods = {}
39
+ @config_methods = {}
40
+ @requires = []
41
+ end
42
+
43
+ # DSL methods for defining plugins
44
+
45
+ def require_file(path)
46
+ @requires << path
47
+ end
48
+
49
+ def cli(mod, require: nil)
50
+ @cli_requires << require if require
51
+ @cli_modules << mod
52
+ end
53
+
54
+ def target_method(name, &block)
55
+ @target_methods[name] = block
56
+ end
57
+
58
+ def config_method(name, &block)
59
+ @config_methods[name] = block
60
+ end
61
+
62
+ # Apply plugin to the system (non-CLI parts)
63
+ def apply!
64
+ @requires.each { |path| require path }
65
+ apply_target_methods!
66
+ apply_config_methods!
67
+ end
68
+
69
+ def apply_to_cli(cli_class)
70
+ @cli_requires.each { |path| require path }
71
+ @cli_modules.each do |mod|
72
+ mod = resolve_constant(mod) if mod.is_a?(String)
73
+ mod.setup(cli_class)
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def apply_target_methods!
80
+ return if @target_methods.empty?
81
+ require "bard/target"
82
+ @target_methods.each do |method_name, block|
83
+ Target.define_method(method_name, &block)
84
+ end
85
+ end
86
+
87
+ def apply_config_methods!
88
+ return if @config_methods.empty?
89
+ require "bard/config"
90
+ @config_methods.each do |method_name, block|
91
+ Config.define_method(method_name, &block)
92
+ end
93
+ end
94
+
95
+ def resolve_constant(name)
96
+ name.split("::").reduce(Object) { |mod, const| mod.const_get(const) }
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,19 @@
1
+ require "bard/plugin"
2
+
3
+ Bard::Plugin.register :backup do
4
+ config_method :backup do |value = nil, &block|
5
+ if block
6
+ @backup = Bard::BackupConfig.new(&block)
7
+ elsif value == false
8
+ @backup = Bard::BackupConfig.new { disabled }
9
+ elsif value.nil? # Getter
10
+ @backup ||= Bard::BackupConfig.new { bard }
11
+ else
12
+ raise ArgumentError, "backup accepts false or a block"
13
+ end
14
+ end
15
+
16
+ config_method :backup_enabled? do
17
+ backup == true
18
+ end
19
+ end
@@ -0,0 +1,34 @@
1
+ require "bard/plugin"
2
+
3
+ # Load the deploy strategy (auto-registers via inherited hook)
4
+ require "bard/deploy_strategy/github_pages"
5
+
6
+ Bard::Plugin.register :github_pages do
7
+ # Config DSL: github_pages "url" sets up a production target
8
+ config_method :github_pages do |url|
9
+ urls = []
10
+ uri = url.start_with?("http") ? URI.parse(url) : URI.parse("https://#{url}")
11
+ hostname = uri.hostname.sub(/^www\./, "")
12
+ urls = [hostname]
13
+ urls << "www.#{hostname}" if hostname.count(".") < 2
14
+
15
+ target :production do
16
+ github_pages url
17
+ ssh false
18
+ ping(*urls) if urls.any?
19
+ end
20
+
21
+ backup false
22
+ end
23
+
24
+ # Target DSL: github_pages sets deploy strategy
25
+ target_method :github_pages do |url = nil|
26
+ if url.nil?
27
+ @github_pages_url
28
+ else
29
+ @deploy_strategy = :github_pages
30
+ @github_pages_url = url
31
+ enable_capability(:github_pages)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,5 @@
1
+ require "bard/plugin"
2
+
3
+ Bard::Plugin.register :hurt do
4
+ cli "Bard::CLI::Hurt", require: "bard/cli/hurt"
5
+ end
@@ -0,0 +1,5 @@
1
+ require "bard/plugin"
2
+
3
+ Bard::Plugin.register :install do
4
+ cli "Bard::CLI::Install", require: "bard/cli/install"
5
+ end
@@ -0,0 +1,6 @@
1
+ require "bard/plugin"
2
+
3
+ Bard::Plugin.register :jenkins do
4
+ # Jenkins CI runner - auto-registers via inherited hook when loaded
5
+ require_file "bard/ci/jenkins"
6
+ end
@@ -0,0 +1,5 @@
1
+ require "bard/plugin"
2
+
3
+ Bard::Plugin.register :new do
4
+ cli "Bard::CLI::New", require: "bard/cli/new"
5
+ end
@@ -0,0 +1,6 @@
1
+ require "bard/plugin"
2
+
3
+ Bard::Plugin.register :ping do
4
+ cli "Bard::CLI::Ping", require: "bard/cli/ping"
5
+ cli "Bard::CLI::Open", require: "bard/cli/open"
6
+ end
@@ -0,0 +1,5 @@
1
+ require "bard/plugin"
2
+
3
+ Bard::Plugin.register :provision do
4
+ cli "Bard::CLI::Provision", require: "bard/cli/provision"
5
+ end
@@ -0,0 +1,5 @@
1
+ require "bard/plugin"
2
+
3
+ Bard::Plugin.register :vim do
4
+ cli "Bard::CLI::Vim", require: "bard/cli/vim"
5
+ end
@@ -0,0 +1,10 @@
1
+ module Bard
2
+ module Secrets
3
+ REPO = "git@github.com:botandrosedesign/secrets"
4
+
5
+ def self.fetch(key)
6
+ raw = `git ls-remote -t #{REPO}`
7
+ raw[/#{Regexp.escape(key)}\|(.+)$/, 1]
8
+ end
9
+ end
10
+ end
data/lib/bard/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Bard
2
- VERSION = "1.8.0"
2
+ VERSION = "1.9.0"
3
3
  end
4
4
 
@@ -1,35 +1,138 @@
1
+ require "spec_helper"
1
2
  require "bard/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/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/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/ci/local"
13
+ require "bard/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