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.
- checksums.yaml +4 -4
- data/CLAUDE.md +76 -0
- data/PLUGINS.md +114 -0
- data/README.md +14 -6
- data/features/ci.feature +62 -0
- data/features/deploy_git_workflow.feature +88 -0
- data/features/step_definitions/bard_steps.rb +96 -0
- data/features/support/bard-coverage +16 -0
- data/features/support/env.rb +10 -1
- data/features/support/test_server.rb +2 -1
- data/lib/bard/ci/github_actions.rb +1 -2
- data/lib/bard/ci/jenkins.rb +82 -11
- data/lib/bard/ci/local.rb +6 -6
- data/lib/bard/ci/runner.rb +35 -1
- data/lib/bard/ci.rb +11 -23
- data/lib/bard/cli/ci.rb +45 -38
- data/lib/bard/cli/deploy.rb +40 -8
- data/lib/bard/cli/hurt.rb +10 -15
- data/lib/bard/cli/install.rb +7 -12
- data/lib/bard/cli/open.rb +12 -16
- data/lib/bard/cli/ping.rb +8 -14
- data/lib/bard/cli/run.rb +5 -3
- data/lib/bard/cli/vim.rb +5 -10
- data/lib/bard/cli.rb +7 -12
- data/lib/bard/config.rb +1 -13
- data/lib/bard/github.rb +2 -4
- data/lib/bard/plugin.rb +99 -0
- data/lib/bard/plugins/backup.rb +19 -0
- data/lib/bard/plugins/github_pages.rb +34 -0
- data/lib/bard/plugins/hurt.rb +5 -0
- data/lib/bard/plugins/install.rb +5 -0
- data/lib/bard/plugins/jenkins.rb +6 -0
- data/lib/bard/plugins/new.rb +5 -0
- data/lib/bard/plugins/ping.rb +6 -0
- data/lib/bard/plugins/provision.rb +5 -0
- data/lib/bard/plugins/vim.rb +5 -0
- data/lib/bard/secrets.rb +10 -0
- data/lib/bard/version.rb +1 -1
- data/spec/bard/ci/github_actions_spec.rb +116 -13
- data/spec/bard/ci/jenkins_spec.rb +139 -0
- data/spec/bard/ci/runner_spec.rb +61 -0
- data/spec/bard/cli/ci_spec.rb +34 -8
- data/spec/bard/cli/deploy_spec.rb +20 -8
- data/spec/bard/cli/hurt_spec.rb +2 -2
- data/spec/bard/cli/install_spec.rb +4 -4
- data/spec/bard/cli/open_spec.rb +10 -8
- data/spec/bard/cli/ping_spec.rb +5 -5
- data/spec/bard/cli/run_spec.rb +20 -1
- data/spec/bard/cli/vim_spec.rb +5 -5
- data/spec/bard/github_spec.rb +1 -1
- data/spec/bard/plugin_spec.rb +79 -0
- data/spec/spec_helper.rb +6 -1
- metadata +27 -3
- 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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 ||=
|
|
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
|
data/lib/bard/plugin.rb
ADDED
|
@@ -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
|
data/lib/bard/secrets.rb
ADDED
data/lib/bard/version.rb
CHANGED
|
@@ -1,35 +1,138 @@
|
|
|
1
|
+
require "spec_helper"
|
|
1
2
|
require "bard/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/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
|