local_ci 0.0.3 → 0.0.4
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/Gemfile.lock +4 -4
- data/Rakefile +0 -2
- data/lib/local_ci/failure.rb +1 -9
- data/lib/local_ci/flow.rb +8 -22
- data/lib/local_ci/helper.rb +3 -2
- data/lib/local_ci/job.rb +20 -9
- data/lib/local_ci/output.rb +150 -0
- data/lib/local_ci.rb +5 -1
- data/local_ci.gemspec +3 -2
- data/spec/unit/local_ci/failure_spec.rb +5 -17
- data/spec/unit/local_ci/flow_spec.rb +7 -3
- data/spec/unit/local_ci/helper_spec.rb +3 -1
- data/spec/unit/local_ci/job_spec.rb +51 -6
- data/spec/unit/local_ci/output_spec.rb +406 -0
- metadata +18 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 408a3e34c26196b5f280ffb77aa68162ea4118a13309d29365bb4e064ccc940f
|
|
4
|
+
data.tar.gz: 893f1167a5a294e58f9f8d27452c2e85879263cbc4e9cbe934ef555a760eb257
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d5e5b78327fa679adf3b29a7b658a7134713d07dd20abb19dde41130d373b490bb00c1241987886beafaa8dbb4c47abf64b0ab07a513f412dcbb3495f09eaef4
|
|
7
|
+
data.tar.gz: c36f886be0c3ddd7f98d24d850c89dabc2962cdf286baa8fde1fd927deab952ea5bc39b174609882e80992f8742546e62cb5f79b129b87ae3b85e399a7c48f5d
|
data/Gemfile.lock
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
local_ci (0.0.
|
|
4
|
+
local_ci (0.0.4)
|
|
5
5
|
logger
|
|
6
6
|
pastel
|
|
7
7
|
rake
|
|
8
8
|
tty-command
|
|
9
|
-
tty-
|
|
9
|
+
tty-cursor
|
|
10
|
+
tty-screen
|
|
10
11
|
|
|
11
12
|
GEM
|
|
12
13
|
remote: https://rubygems.org/
|
|
@@ -78,8 +79,7 @@ GEM
|
|
|
78
79
|
tty-command (0.10.1)
|
|
79
80
|
pastel (~> 0.8)
|
|
80
81
|
tty-cursor (0.7.1)
|
|
81
|
-
tty-
|
|
82
|
-
tty-cursor (~> 0.7)
|
|
82
|
+
tty-screen (0.8.2)
|
|
83
83
|
unicode-display_width (3.2.0)
|
|
84
84
|
unicode-emoji (~> 4.1)
|
|
85
85
|
unicode-emoji (4.1.0)
|
data/Rakefile
CHANGED
data/lib/local_ci/failure.rb
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
module LocalCI
|
|
2
2
|
class Failure
|
|
3
|
-
attr_reader :job
|
|
3
|
+
attr_reader :job, :message
|
|
4
4
|
|
|
5
5
|
def initialize(job:, message:)
|
|
6
6
|
@job = job
|
|
7
7
|
@message = message
|
|
8
8
|
end
|
|
9
|
-
|
|
10
|
-
def message
|
|
11
|
-
<<~STR
|
|
12
|
-
#{LocalCI::Helper.pastel.bold.red("FAIL:")} #{@job}
|
|
13
|
-
#{@message}
|
|
14
|
-
|
|
15
|
-
STR
|
|
16
|
-
end
|
|
17
9
|
end
|
|
18
10
|
end
|
data/lib/local_ci/flow.rb
CHANGED
|
@@ -1,25 +1,17 @@
|
|
|
1
1
|
module LocalCI
|
|
2
2
|
class Flow
|
|
3
|
-
attr_accessor :task, :heading, :spinner, :failures
|
|
3
|
+
attr_accessor :task, :heading, :spinner, :failures, :jobs, :output
|
|
4
4
|
|
|
5
5
|
def initialize(name:, heading:, parallel:, block:, actions: true)
|
|
6
6
|
@task = name
|
|
7
7
|
@task = "ci:#{name}" unless @task.start_with?("ci:")
|
|
8
8
|
@parallel = parallel
|
|
9
9
|
@failures = []
|
|
10
|
-
@heading = heading
|
|
11
10
|
@actions = actions
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
error_mark: LocalCI::Helper.pastel.red("✗"),
|
|
17
|
-
style: {
|
|
18
|
-
top: "",
|
|
19
|
-
middle: " ",
|
|
20
|
-
bottom: " "
|
|
21
|
-
}
|
|
22
|
-
)
|
|
11
|
+
|
|
12
|
+
@heading = heading
|
|
13
|
+
@jobs = []
|
|
14
|
+
@output = LocalCI::Output.new(flow: self)
|
|
23
15
|
|
|
24
16
|
setup_required_tasks
|
|
25
17
|
if actions?
|
|
@@ -59,14 +51,12 @@ module LocalCI
|
|
|
59
51
|
LocalCI::Task.new("#{@task}:jobs", parallel_prerequisites: @parallel)
|
|
60
52
|
|
|
61
53
|
LocalCI::Task["#{@task}:teardown"]
|
|
62
|
-
LocalCI::Task["#{@task}:failure_check"]
|
|
63
54
|
LocalCI::Task[@task, @heading]
|
|
64
55
|
|
|
65
56
|
LocalCI::Task["#{@task}:setup"]
|
|
66
57
|
LocalCI::Task["#{@task}:setup"].add_prerequisite "ci:setup"
|
|
67
58
|
|
|
68
59
|
LocalCI::Task[@task].add_prerequisite "#{@task}:teardown"
|
|
69
|
-
LocalCI::Task["#{@task}:failure_check"].add_prerequisite "#{@task}:teardown"
|
|
70
60
|
LocalCI::Task["#{@task}:teardown"].add_prerequisite "#{@task}:jobs"
|
|
71
61
|
LocalCI::Task["#{@task}:jobs"].add_prerequisite "#{@task}:setup"
|
|
72
62
|
|
|
@@ -75,10 +65,8 @@ module LocalCI
|
|
|
75
65
|
|
|
76
66
|
def setup_actionless_flow_tasks
|
|
77
67
|
LocalCI::Task.new("#{@task}:jobs", parallel_prerequisites: @parallel)
|
|
78
|
-
LocalCI::Task["#{@task}:failure_check"]
|
|
79
68
|
|
|
80
|
-
LocalCI::Task[@task].add_prerequisite "#{@task}:
|
|
81
|
-
LocalCI::Task["#{@task}:failure_check"].add_prerequisite "#{@task}:jobs"
|
|
69
|
+
LocalCI::Task[@task].add_prerequisite "#{@task}:jobs"
|
|
82
70
|
end
|
|
83
71
|
|
|
84
72
|
def after_jobs
|
|
@@ -88,11 +76,9 @@ module LocalCI
|
|
|
88
76
|
|
|
89
77
|
if @failures.any?
|
|
90
78
|
LocalCI::Task["ci:teardown"].invoke
|
|
91
|
-
|
|
92
|
-
puts failure.message
|
|
93
|
-
end
|
|
79
|
+
output.failures
|
|
94
80
|
|
|
95
|
-
abort LocalCI::Helper.pastel.red("#{@heading} failed, see
|
|
81
|
+
abort LocalCI::Helper.pastel.red("#{@heading} failed, see ci.log for more.")
|
|
96
82
|
end
|
|
97
83
|
end
|
|
98
84
|
end
|
data/lib/local_ci/helper.rb
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
module LocalCI
|
|
2
2
|
module Helper
|
|
3
|
-
def self.
|
|
4
|
-
def self.
|
|
3
|
+
def self.color? = TTY::Color.support?
|
|
4
|
+
def self.pastel = @pastel ||= Pastel.new(enabled: color?)
|
|
5
|
+
def self.runner = @runner ||= TTY::Command.new(color: color?, output: Logger.new("ci.log"))
|
|
5
6
|
|
|
6
7
|
def self.taskize(heading)
|
|
7
8
|
heading.downcase.gsub(/\s/, "_").gsub(/[^\w]/, "").to_sym
|
data/lib/local_ci/job.rb
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
module LocalCI
|
|
2
2
|
class Job
|
|
3
|
-
|
|
3
|
+
attr_reader :flow, :name, :command, :block, :task, :state, :duration
|
|
4
4
|
|
|
5
5
|
def initialize(flow:, name:, command:, block:)
|
|
6
6
|
@flow = flow
|
|
7
|
-
@
|
|
7
|
+
@flow.jobs << self
|
|
8
|
+
@task = "#{@flow.task}:jobs:#{LocalCI::Helper.taskize(name)}"
|
|
9
|
+
|
|
8
10
|
@command = command
|
|
9
11
|
@block = block
|
|
10
|
-
|
|
12
|
+
|
|
13
|
+
@name = name
|
|
14
|
+
@state = :waiting
|
|
11
15
|
|
|
12
16
|
raise ArgumentError, "Must specify a block or command" unless command || block
|
|
13
|
-
@spinner = flow.spinner.register("[:spinner] #{name}")
|
|
14
17
|
|
|
15
18
|
::Rake::Task.define_task(task) do
|
|
16
|
-
@
|
|
19
|
+
@state = :running
|
|
20
|
+
@flow.output.update(self)
|
|
17
21
|
start = Time.now
|
|
18
22
|
|
|
19
23
|
if block
|
|
@@ -21,20 +25,27 @@ module LocalCI
|
|
|
21
25
|
else
|
|
22
26
|
LocalCI::Helper.runner.run(*command)
|
|
23
27
|
end
|
|
24
|
-
|
|
25
|
-
took = Time.now - start
|
|
26
|
-
@spinner.success("(#{took.round(2)}s)")
|
|
28
|
+
@state = :success
|
|
27
29
|
rescue TTY::Command::ExitError => e
|
|
28
|
-
|
|
30
|
+
@state = :failed
|
|
29
31
|
@flow.failures << LocalCI::Failure.new(
|
|
30
32
|
job: @name,
|
|
31
33
|
message: e.message
|
|
32
34
|
)
|
|
35
|
+
ensure
|
|
36
|
+
@duration = Time.now - start
|
|
37
|
+
@flow.output.update(self)
|
|
33
38
|
end
|
|
34
39
|
|
|
35
40
|
::Rake::Task["#{@flow.task}:jobs"].prerequisites << task
|
|
36
41
|
|
|
37
42
|
::Rake::Task[task].prerequisites << "#{@flow.task}:setup" if @flow.actions?
|
|
38
43
|
end
|
|
44
|
+
|
|
45
|
+
def waiting? = @state == :waiting
|
|
46
|
+
def running? = @state == :running
|
|
47
|
+
def success? = @state == :success
|
|
48
|
+
def failed? = @state == :failed
|
|
49
|
+
def done? = [:success, :failed].include? @state
|
|
39
50
|
end
|
|
40
51
|
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
module LocalCI
|
|
2
|
+
class Output
|
|
3
|
+
attr_reader :job
|
|
4
|
+
|
|
5
|
+
def initialize(flow:)
|
|
6
|
+
@flow = flow
|
|
7
|
+
@first_paint = true
|
|
8
|
+
@start = 0
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def start_thread
|
|
13
|
+
@thread = Thread.new {
|
|
14
|
+
loop do
|
|
15
|
+
@mutex.synchronize { draw(final: @thread_should_exit) }
|
|
16
|
+
break if @thread_should_exit
|
|
17
|
+
sleep 0.1
|
|
18
|
+
end
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def finish
|
|
23
|
+
@thread_should_exit = true
|
|
24
|
+
@thread.join
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def pastel = LocalCI::Helper.pastel
|
|
28
|
+
def cursor = TTY::Cursor
|
|
29
|
+
def screen = TTY::Screen
|
|
30
|
+
def tty? = $stdout.isatty
|
|
31
|
+
|
|
32
|
+
def passed? = @flow.jobs.all?(&:success?)
|
|
33
|
+
def failed? = @flow.failures.any?
|
|
34
|
+
def done? = @flow.jobs.all?(&:done?)
|
|
35
|
+
|
|
36
|
+
def update(job)
|
|
37
|
+
if tty?
|
|
38
|
+
finish and return if done?
|
|
39
|
+
|
|
40
|
+
return if @thread&.alive?
|
|
41
|
+
|
|
42
|
+
start_thread
|
|
43
|
+
else
|
|
44
|
+
@job = job
|
|
45
|
+
|
|
46
|
+
@mutex.synchronize { json_output }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def failures
|
|
51
|
+
return unless tty?
|
|
52
|
+
|
|
53
|
+
@mutex.synchronize {
|
|
54
|
+
@flow.failures.each do |failure|
|
|
55
|
+
puts <<~STR
|
|
56
|
+
#{pastel.bold.red("FAIL:")} #{failure.job}
|
|
57
|
+
#{failure.message}
|
|
58
|
+
|
|
59
|
+
STR
|
|
60
|
+
end
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def draw(final: false)
|
|
65
|
+
if @first_paint
|
|
66
|
+
@start = Time.now
|
|
67
|
+
@first_paint = false
|
|
68
|
+
else
|
|
69
|
+
print cursor.clear_line + cursor.up(@flow.jobs.size + 2)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
puts heading_line
|
|
73
|
+
@flow.jobs.each { puts job_line it }
|
|
74
|
+
puts footer_line
|
|
75
|
+
|
|
76
|
+
puts if final
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def color(message)
|
|
80
|
+
if passed?
|
|
81
|
+
pastel.green(message)
|
|
82
|
+
elsif failed?
|
|
83
|
+
pastel.red(message)
|
|
84
|
+
else
|
|
85
|
+
pastel.blue(message)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def heading_line
|
|
90
|
+
heading = @flow.heading.dup
|
|
91
|
+
heading = "#{heading[...(screen.width - 11)]}…" if heading.length > (screen.width - 10)
|
|
92
|
+
|
|
93
|
+
tail_length = screen.width - 5 - heading.length - 2
|
|
94
|
+
|
|
95
|
+
color "===| #{pastel.bold heading} |#{"=" * tail_length}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def job_line(job)
|
|
99
|
+
name = job.name.dup
|
|
100
|
+
|
|
101
|
+
name = "#{name[...(screen.width - 4)]}…" if name.length > (screen.width - 3)
|
|
102
|
+
|
|
103
|
+
result = cursor.clear_line
|
|
104
|
+
if job.waiting?
|
|
105
|
+
result << "[ ] #{name}"
|
|
106
|
+
elsif job.running?
|
|
107
|
+
result << "[-] #{name}"
|
|
108
|
+
elsif job.success?
|
|
109
|
+
result << "[#{pastel.green "✓"}] #{name}"
|
|
110
|
+
elsif job.failed?
|
|
111
|
+
result << "[#{pastel.red "✗"}] #{name}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
result
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def duration
|
|
118
|
+
seconds = Time.now - @start
|
|
119
|
+
minutes = seconds / 60
|
|
120
|
+
hours = minutes / 60
|
|
121
|
+
|
|
122
|
+
seconds %= 60
|
|
123
|
+
minutes %= 60
|
|
124
|
+
|
|
125
|
+
if hours >= 1
|
|
126
|
+
"#{hours}h #{minutes}m"
|
|
127
|
+
|
|
128
|
+
elsif minutes >= 1
|
|
129
|
+
"#{minutes}m #{seconds}s"
|
|
130
|
+
|
|
131
|
+
else
|
|
132
|
+
"#{seconds.round(2)}s"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def footer_line
|
|
137
|
+
start_length = screen.width - 4 - duration.size - 1
|
|
138
|
+
color "#{"-" * start_length}(#{pastel.bold duration})---"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def json_output
|
|
142
|
+
puts({
|
|
143
|
+
flow: @flow.heading,
|
|
144
|
+
job: @job.name,
|
|
145
|
+
duration: @job.duration,
|
|
146
|
+
state: @job.state
|
|
147
|
+
}.to_json)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
data/lib/local_ci.rb
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
require "forwardable"
|
|
2
|
+
require "json"
|
|
2
3
|
require "logger"
|
|
4
|
+
|
|
3
5
|
require "tty-command"
|
|
4
|
-
require "tty-
|
|
6
|
+
require "tty-cursor"
|
|
7
|
+
require "tty-screen"
|
|
5
8
|
require "pastel"
|
|
6
9
|
|
|
7
10
|
require "local_ci/dsl"
|
|
@@ -10,5 +13,6 @@ require "local_ci/failure"
|
|
|
10
13
|
require "local_ci/flow"
|
|
11
14
|
require "local_ci/helper"
|
|
12
15
|
require "local_ci/job"
|
|
16
|
+
require "local_ci/output"
|
|
13
17
|
require "local_ci/rake"
|
|
14
18
|
require "local_ci/task"
|
data/local_ci.gemspec
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Gem::Specification.new do |s|
|
|
2
2
|
s.name = "local_ci"
|
|
3
|
-
s.version = "0.0.
|
|
3
|
+
s.version = "0.0.4"
|
|
4
4
|
s.summary = "Run CI locally"
|
|
5
5
|
s.description = "A way to run CI locally but also able to run easily on your hosted CI"
|
|
6
6
|
s.authors = ["Sean Earle"]
|
|
@@ -14,5 +14,6 @@ Gem::Specification.new do |s|
|
|
|
14
14
|
s.add_dependency "pastel"
|
|
15
15
|
s.add_dependency "rake"
|
|
16
16
|
s.add_dependency "tty-command"
|
|
17
|
-
s.add_dependency "tty-
|
|
17
|
+
s.add_dependency "tty-cursor"
|
|
18
|
+
s.add_dependency "tty-screen"
|
|
18
19
|
end
|
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
require "spec_helper"
|
|
2
2
|
|
|
3
3
|
describe LocalCI::Failure do
|
|
4
|
-
describe "#
|
|
5
|
-
it "
|
|
6
|
-
|
|
7
|
-
expect(LocalCI::Helper).to receive(:pastel).and_return(pastel)
|
|
8
|
-
expect(pastel).to receive(:bold).and_return(pastel)
|
|
9
|
-
expect(pastel).to receive(:red).with("FAIL:")
|
|
10
|
-
.and_return("red")
|
|
4
|
+
describe "#initialize" do
|
|
5
|
+
it "sets up the instance variables" do
|
|
6
|
+
failure = LocalCI::Failure.new(job: "job", message: "message")
|
|
11
7
|
|
|
12
|
-
failure
|
|
13
|
-
|
|
14
|
-
message: "message"
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
expect(failure.message).to eq(<<~STR)
|
|
18
|
-
red job
|
|
19
|
-
message
|
|
20
|
-
|
|
21
|
-
STR
|
|
8
|
+
expect(failure.job).to eq("job")
|
|
9
|
+
expect(failure.message).to eq("message")
|
|
22
10
|
end
|
|
23
11
|
end
|
|
24
12
|
end
|
|
@@ -187,10 +187,14 @@ describe LocalCI::Flow do
|
|
|
187
187
|
|
|
188
188
|
context "when there are failures" do
|
|
189
189
|
before do
|
|
190
|
-
@failure = double(:failure, message: "hi")
|
|
190
|
+
@failure = double(:failure, job: "job", message: "hi")
|
|
191
191
|
@flow.failures << @failure
|
|
192
192
|
|
|
193
193
|
allow(@flow).to receive(:abort)
|
|
194
|
+
|
|
195
|
+
@output = double(:output)
|
|
196
|
+
allow(@output).to receive(:failures)
|
|
197
|
+
allow(@flow).to receive(:output).and_return(@output)
|
|
194
198
|
end
|
|
195
199
|
|
|
196
200
|
it "runs ci:flow:teardown" do
|
|
@@ -206,13 +210,13 @@ describe LocalCI::Flow do
|
|
|
206
210
|
end
|
|
207
211
|
|
|
208
212
|
it "displays the failures" do
|
|
209
|
-
expect(@
|
|
213
|
+
expect(@output).to receive(:failures)
|
|
210
214
|
|
|
211
215
|
::Rake::Task["ci:flow"].invoke
|
|
212
216
|
end
|
|
213
217
|
|
|
214
218
|
it "aborts with a message" do
|
|
215
|
-
expect(@flow).to receive(:abort).with(/heading failed, see
|
|
219
|
+
expect(@flow).to receive(:abort).with(/heading failed, see ci\.log for more\./)
|
|
216
220
|
|
|
217
221
|
::Rake::Task["ci:flow"].invoke
|
|
218
222
|
end
|
|
@@ -13,8 +13,10 @@ describe LocalCI::Helper do
|
|
|
13
13
|
|
|
14
14
|
describe ".runner" do
|
|
15
15
|
it "returns a TTY::Command instance" do
|
|
16
|
+
expect(TTY::Color).to receive(:support?).and_return("support")
|
|
16
17
|
expect(Logger).to receive(:new).with("ci.log").and_return("logger")
|
|
17
|
-
expect(TTY::Command).to receive(:new)
|
|
18
|
+
expect(TTY::Command).to receive(:new)
|
|
19
|
+
.with(color: "support", output: "logger")
|
|
18
20
|
.and_return("tty-command")
|
|
19
21
|
|
|
20
22
|
expect(LocalCI::Helper.runner).to eq("tty-command")
|
|
@@ -2,12 +2,16 @@ require "spec_helper"
|
|
|
2
2
|
|
|
3
3
|
describe LocalCI::Job do
|
|
4
4
|
before do
|
|
5
|
+
@output = double(:output)
|
|
6
|
+
allow(@output).to receive(:update)
|
|
7
|
+
|
|
5
8
|
@flow = LocalCI::Flow.new(
|
|
6
9
|
name: "flow",
|
|
7
10
|
heading: "heading",
|
|
8
11
|
parallel: true,
|
|
9
12
|
block: -> {}
|
|
10
13
|
)
|
|
14
|
+
allow(@flow).to receive(:output).and_return(@output)
|
|
11
15
|
end
|
|
12
16
|
|
|
13
17
|
describe "#initialize" do
|
|
@@ -24,6 +28,7 @@ describe LocalCI::Job do
|
|
|
24
28
|
expect(job.command).to eq("command")
|
|
25
29
|
expect(job.block).to eq("block")
|
|
26
30
|
expect(job.task).to eq("ci:flow:jobs:the_job_name")
|
|
31
|
+
expect(job.state).to eq(:waiting)
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
context "when passed no block or command" do
|
|
@@ -80,6 +85,31 @@ describe LocalCI::Job do
|
|
|
80
85
|
|
|
81
86
|
::Rake::Task["ci:flow:jobs:block_task"].invoke
|
|
82
87
|
end
|
|
88
|
+
|
|
89
|
+
it "sets the state" do
|
|
90
|
+
job = LocalCI::Job.new(
|
|
91
|
+
flow: @flow,
|
|
92
|
+
name: "block task",
|
|
93
|
+
command: nil,
|
|
94
|
+
block: -> {}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
::Rake::Task["ci:flow:jobs:block_task"].invoke
|
|
98
|
+
|
|
99
|
+
expect(job.state).to eq(:success)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "updates the output" do
|
|
103
|
+
job = LocalCI::Job.new(
|
|
104
|
+
flow: @flow,
|
|
105
|
+
name: "block task",
|
|
106
|
+
command: nil,
|
|
107
|
+
block: -> {}
|
|
108
|
+
)
|
|
109
|
+
expect(@output).to receive(:update).with(job)
|
|
110
|
+
|
|
111
|
+
::Rake::Task["ci:flow:jobs:block_task"].invoke
|
|
112
|
+
end
|
|
83
113
|
end
|
|
84
114
|
|
|
85
115
|
context "when running one command inline" do
|
|
@@ -97,6 +127,15 @@ describe LocalCI::Job do
|
|
|
97
127
|
end
|
|
98
128
|
|
|
99
129
|
context "when the job fails" do
|
|
130
|
+
before do
|
|
131
|
+
@job = LocalCI::Job.new(
|
|
132
|
+
flow: @flow,
|
|
133
|
+
name: "Raises an Error",
|
|
134
|
+
command: "exit 1",
|
|
135
|
+
block: nil
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
100
139
|
it "is recorded against the flow" do
|
|
101
140
|
expect(LocalCI::Helper.runner).to receive(:run).with("exit 1")
|
|
102
141
|
.and_raise(
|
|
@@ -110,17 +149,23 @@ describe LocalCI::Job do
|
|
|
110
149
|
)
|
|
111
150
|
)
|
|
112
151
|
|
|
113
|
-
LocalCI::Job.new(
|
|
114
|
-
flow: @flow,
|
|
115
|
-
name: "Raises an Error",
|
|
116
|
-
command: "exit 1",
|
|
117
|
-
block: nil
|
|
118
|
-
)
|
|
119
152
|
::Rake::Task["ci:flow:jobs:raises_an_error"].invoke
|
|
120
153
|
|
|
121
154
|
expect(@flow.failures.size).to eq(1)
|
|
122
155
|
expect(@flow.failures.first.job).to eq("Raises an Error")
|
|
123
156
|
end
|
|
157
|
+
|
|
158
|
+
it "sets the state" do
|
|
159
|
+
::Rake::Task["ci:flow:jobs:raises_an_error"].invoke
|
|
160
|
+
|
|
161
|
+
expect(@job.state).to eq(:failed)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it "updates the output" do
|
|
165
|
+
expect(@output).to receive(:update).with(@job)
|
|
166
|
+
|
|
167
|
+
::Rake::Task["ci:flow:jobs:raises_an_error"].invoke
|
|
168
|
+
end
|
|
124
169
|
end
|
|
125
170
|
end
|
|
126
171
|
end
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
describe LocalCI::Output do
|
|
4
|
+
before do
|
|
5
|
+
@job = double(:job)
|
|
6
|
+
@flow = double(:flow, jobs: [])
|
|
7
|
+
@thread = double(:thread, alive?: true)
|
|
8
|
+
allow(@thread).to receive(:join)
|
|
9
|
+
|
|
10
|
+
@output = LocalCI::Output.new(flow: @flow)
|
|
11
|
+
@output.instance_variable_set(:@thread, @thread)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe "#update" do
|
|
15
|
+
before do
|
|
16
|
+
allow(@output).to receive(:output)
|
|
17
|
+
allow(@output).to receive(:json_output)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
context "when we are not in a TTY" do
|
|
21
|
+
before do
|
|
22
|
+
expect(@output).to receive(:tty?).and_return(false)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "sets the job" do
|
|
26
|
+
@output.update(@job)
|
|
27
|
+
expect(@output.job).to eq(@job)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "calls json_output" do
|
|
31
|
+
expect(@output).to receive(:json_output)
|
|
32
|
+
@output.update(@job)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
context "when we are in a TTY" do
|
|
37
|
+
before do
|
|
38
|
+
allow(@output).to receive(:tty?).and_return(true)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
context "when all jobs are done" do
|
|
42
|
+
before do
|
|
43
|
+
allow(@output).to receive(:done?).and_return(true)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "calls finish" do
|
|
47
|
+
expect(@output).to receive(:finish)
|
|
48
|
+
@output.update(@job)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "does not call start_thread" do
|
|
52
|
+
expect(@output).not_to receive(:start_thread)
|
|
53
|
+
@output.update(@job)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
context "the thread is alive" do
|
|
58
|
+
before do
|
|
59
|
+
allow(@output).to receive(:done?).and_return(false)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "does not call finish" do
|
|
63
|
+
expect(@output).not_to receive(:finish)
|
|
64
|
+
@output.update(@job)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "does not call start_thread" do
|
|
68
|
+
expect(@output).not_to receive(:start_thread)
|
|
69
|
+
@output.update(@job)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
context "the thread is not alive" do
|
|
74
|
+
before do
|
|
75
|
+
allow(@output).to receive(:done?).and_return(false)
|
|
76
|
+
allow(@thread).to receive(:alive?).and_return(false)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "starts the thread" do
|
|
80
|
+
expect(@output).to receive(:start_thread)
|
|
81
|
+
|
|
82
|
+
@output.update(@job)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
describe "#failures" do
|
|
89
|
+
before do
|
|
90
|
+
allow(@flow).to receive(:failures).and_return(
|
|
91
|
+
[
|
|
92
|
+
double(:failure, job: "job-1", message: "message-1"),
|
|
93
|
+
double(:failure, job: "job-2", message: "message-2")
|
|
94
|
+
]
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
context "when we are in a TTY" do
|
|
99
|
+
it "displays the errors" do
|
|
100
|
+
expect(@output).to receive(:tty?).and_return(true)
|
|
101
|
+
|
|
102
|
+
expect(@output).to receive(:puts).with(/job-1\nmessage-1/)
|
|
103
|
+
expect(@output).to receive(:puts).with(/job-2\nmessage-2/)
|
|
104
|
+
|
|
105
|
+
@output.failures
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
context "when we are not in a TTY" do
|
|
110
|
+
it "does nothing" do
|
|
111
|
+
expect(@output).to receive(:tty?).and_return(false)
|
|
112
|
+
|
|
113
|
+
expect(@output).not_to receive(:puts)
|
|
114
|
+
|
|
115
|
+
@output.failures
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
describe "#draw" do
|
|
121
|
+
before do
|
|
122
|
+
allow(@output).to receive(:print)
|
|
123
|
+
allow(@output).to receive(:puts)
|
|
124
|
+
allow(@output).to receive(:heading_line).and_return("heading-line")
|
|
125
|
+
allow(@output).to receive(:job_line).and_return("job-line")
|
|
126
|
+
allow(@output).to receive(:footer_line).and_return("footer-line")
|
|
127
|
+
|
|
128
|
+
allow(@flow).to receive(:jobs).and_return(["job-1", "job-2"])
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
context "on first paint for the flow" do
|
|
132
|
+
before do
|
|
133
|
+
@output.instance_variable_set(:@first_paint, true)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it "does not clear lines" do
|
|
137
|
+
expect(TTY::Cursor).not_to receive(:clear_line)
|
|
138
|
+
expect(TTY::Cursor).not_to receive(:up)
|
|
139
|
+
|
|
140
|
+
@output.draw
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "sets the start time" do
|
|
144
|
+
expect(Time).to receive(:now).and_return("time")
|
|
145
|
+
|
|
146
|
+
@output.draw
|
|
147
|
+
|
|
148
|
+
expect(@output.instance_variable_get(:@start)).to eq("time")
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
context "on subsequent paints" do
|
|
153
|
+
before do
|
|
154
|
+
@output.instance_variable_set(:@first_paint, false)
|
|
155
|
+
|
|
156
|
+
allow(TTY::Cursor).to receive(:clear_line).and_return("")
|
|
157
|
+
allow(TTY::Cursor).to receive(:up).and_return("")
|
|
158
|
+
allow(@output).to receive(:print)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it "moves up and clears the line" do
|
|
162
|
+
expect(TTY::Cursor).to receive(:clear_line).and_return("clear-line")
|
|
163
|
+
expect(TTY::Cursor).to receive(:up).with(4).and_return("up")
|
|
164
|
+
|
|
165
|
+
expect(@output).to receive(:print).with("clear-lineup")
|
|
166
|
+
|
|
167
|
+
@output.draw
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it "paints the heading" do
|
|
172
|
+
expect(@output).to receive(:heading_line).and_return("heading-line")
|
|
173
|
+
|
|
174
|
+
expect(@output).to receive(:puts).with("heading-line")
|
|
175
|
+
|
|
176
|
+
@output.draw
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
it "paints the jobs" do
|
|
180
|
+
expect(@output).to receive(:job_line).with("job-1").and_return("job-line-1")
|
|
181
|
+
expect(@output).to receive(:job_line).with("job-2").and_return("job-line-2")
|
|
182
|
+
|
|
183
|
+
expect(@output).to receive(:puts).with("job-line-1")
|
|
184
|
+
expect(@output).to receive(:puts).with("job-line-2")
|
|
185
|
+
|
|
186
|
+
@output.draw
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it "paints the footer" do
|
|
190
|
+
expect(@output).to receive(:footer_line).and_return("footer-line")
|
|
191
|
+
|
|
192
|
+
expect(@output).to receive(:puts).with("footer-line")
|
|
193
|
+
|
|
194
|
+
@output.draw
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
context "when final is true" do
|
|
198
|
+
it "draws an extra new line" do
|
|
199
|
+
expect(@output).to receive(:puts).with(no_args)
|
|
200
|
+
|
|
201
|
+
@output.draw(final: true)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
describe "#color" do
|
|
207
|
+
before do
|
|
208
|
+
@pastel = double(:pastel)
|
|
209
|
+
allow(@output).to receive(:pastel).and_return(@pastel)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
context "when the flow is running" do
|
|
213
|
+
it "returns blue" do
|
|
214
|
+
expect(@output).to receive(:passed?).and_return(false)
|
|
215
|
+
expect(@output).to receive(:failed?).and_return(false)
|
|
216
|
+
|
|
217
|
+
expect(@pastel).to receive(:blue).with("message")
|
|
218
|
+
|
|
219
|
+
@output.color("message")
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
context "when the flow has passed" do
|
|
224
|
+
it "returns blue" do
|
|
225
|
+
expect(@output).to receive(:passed?).and_return(true)
|
|
226
|
+
|
|
227
|
+
expect(@pastel).to receive(:green).with("message")
|
|
228
|
+
|
|
229
|
+
@output.color("message")
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
context "when the flow has failed" do
|
|
234
|
+
it "returns blue" do
|
|
235
|
+
expect(@output).to receive(:passed?).and_return(false)
|
|
236
|
+
expect(@output).to receive(:failed?).and_return(true)
|
|
237
|
+
|
|
238
|
+
expect(@pastel).to receive(:red).with("message")
|
|
239
|
+
|
|
240
|
+
@output.color("message")
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
describe "#heading_line" do
|
|
246
|
+
before do
|
|
247
|
+
allow(@flow).to receive(:heading).and_return("flow-heading")
|
|
248
|
+
allow(@output).to receive(:screen).and_return(double(width: 40))
|
|
249
|
+
@pastel = Pastel.new
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
it "returns a heading the width of the terminal" do
|
|
253
|
+
expect(@pastel.strip(@output.heading_line)).to eq(
|
|
254
|
+
"===| flow-heading |====================="
|
|
255
|
+
)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
context "when the heading is too long" do
|
|
259
|
+
it "truncates it with an ellipsis" do
|
|
260
|
+
allow(@flow).to receive(:heading).and_return("Some really long heading that really probably shouldn't be this long but is I guess")
|
|
261
|
+
expect(@pastel.strip(@output.heading_line)).to eq(
|
|
262
|
+
"===| Some really long heading that… |==="
|
|
263
|
+
)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
context "when the heading is just short enough" do
|
|
268
|
+
it "does not truncate" do
|
|
269
|
+
allow(@flow).to receive(:heading).and_return("Something long, but not silly!")
|
|
270
|
+
|
|
271
|
+
expect(@pastel.strip(@output.heading_line)).to eq(
|
|
272
|
+
"===| Something long, but not silly! |==="
|
|
273
|
+
)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
describe "#job_line" do
|
|
279
|
+
before do
|
|
280
|
+
@pastel = Pastel.new
|
|
281
|
+
|
|
282
|
+
@job = double(:job, name: "job-name")
|
|
283
|
+
allow(@job).to receive(:waiting?).and_return(false)
|
|
284
|
+
allow(@job).to receive(:running?).and_return(false)
|
|
285
|
+
allow(@job).to receive(:success?).and_return(false)
|
|
286
|
+
allow(@job).to receive(:failed?).and_return(false)
|
|
287
|
+
|
|
288
|
+
allow(@output).to receive_message_chain(:cursor, :clear_line).and_return("")
|
|
289
|
+
allow(@output).to receive(:screen).and_return(double(width: 20))
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
context "when the job is waiting" do
|
|
293
|
+
it "returns no indicator and the job name" do
|
|
294
|
+
expect(@job).to receive(:waiting?).and_return(true)
|
|
295
|
+
expect(@pastel.strip(@output.job_line(@job))).to eq("[ ] job-name")
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
context "when the job is running" do
|
|
300
|
+
it "returns an indicator and the job name" do
|
|
301
|
+
expect(@job).to receive(:running?).and_return(true)
|
|
302
|
+
expect(@pastel.strip(@output.job_line(@job))).to eq("[-] job-name")
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
context "when the job was successful" do
|
|
307
|
+
it "returns an indicator and the job name" do
|
|
308
|
+
expect(@job).to receive(:success?).and_return(true)
|
|
309
|
+
expect(@pastel.strip(@output.job_line(@job))).to eq("[✓] job-name")
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
context "when the job failed" do
|
|
314
|
+
it "returns an indicator and the job name" do
|
|
315
|
+
expect(@job).to receive(:failed?).and_return(true)
|
|
316
|
+
expect(@pastel.strip(@output.job_line(@job))).to eq("[✗] job-name")
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
context "when the name is too long" do
|
|
321
|
+
it "truncates it with an ellipsis" do
|
|
322
|
+
allow(@job).to receive(:waiting?).and_return(true)
|
|
323
|
+
allow(@job).to receive(:name).and_return("A really long job name, obviously just way too long, what are you even doing!")
|
|
324
|
+
|
|
325
|
+
expect(@pastel.strip(@output.job_line(@job))).to eq(
|
|
326
|
+
"[ ] A really long jo…"
|
|
327
|
+
)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
context "when the name is just short enough" do
|
|
332
|
+
it "does not truncate" do
|
|
333
|
+
allow(@job).to receive(:waiting?).and_return(true)
|
|
334
|
+
allow(@job).to receive(:name).and_return("A long job name!!")
|
|
335
|
+
|
|
336
|
+
expect(@pastel.strip(@output.job_line(@job))).to eq(
|
|
337
|
+
"[ ] A long job name!!"
|
|
338
|
+
)
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
describe "#footer_line" do
|
|
344
|
+
before do
|
|
345
|
+
allow(@output).to receive(:screen).and_return(double(width: 20))
|
|
346
|
+
allow(@output).to receive(:duration).and_return("duration")
|
|
347
|
+
@pastel = Pastel.new
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
it "returns a footer the length of the terminal with the duration" do
|
|
351
|
+
expect(@pastel.strip(@output.footer_line)).to eq(
|
|
352
|
+
"-------(duration)---"
|
|
353
|
+
)
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
describe "#duration" do
|
|
358
|
+
context "when less than 60 seconds" do
|
|
359
|
+
it "shows seconds with two decimal places" do
|
|
360
|
+
@output.instance_variable_set(:@start, 5.567)
|
|
361
|
+
allow(Time).to receive(:now).and_return(45)
|
|
362
|
+
|
|
363
|
+
expect(@output.duration).to eq("39.43s")
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
context "when less than 60 minutes" do
|
|
368
|
+
it "shows the minutes and seconds" do
|
|
369
|
+
@output.instance_variable_set(:@start, 10)
|
|
370
|
+
allow(Time).to receive(:now).and_return(1240)
|
|
371
|
+
|
|
372
|
+
expect(@output.duration).to eq("20m 30s")
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
context "when greater than 60 minutes" do
|
|
377
|
+
it "shows the hours and minutes" do
|
|
378
|
+
@output.instance_variable_set(:@start, 10)
|
|
379
|
+
allow(Time).to receive(:now).and_return(3800)
|
|
380
|
+
|
|
381
|
+
expect(@output.duration).to eq("1h 3m")
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
describe "#json_output" do
|
|
387
|
+
it "returns the state as json" do
|
|
388
|
+
allow(@flow).to receive(:heading).and_return("flow-heading")
|
|
389
|
+
@output.instance_variable_set(:@job, double(
|
|
390
|
+
:job,
|
|
391
|
+
name: "job-name",
|
|
392
|
+
duration: "job-duration",
|
|
393
|
+
state: "job-state"
|
|
394
|
+
))
|
|
395
|
+
|
|
396
|
+
expect(@output).to receive(:puts).with({
|
|
397
|
+
flow: "flow-heading",
|
|
398
|
+
job: "job-name",
|
|
399
|
+
duration: "job-duration",
|
|
400
|
+
state: "job-state"
|
|
401
|
+
}.to_json)
|
|
402
|
+
|
|
403
|
+
@output.json_output
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: local_ci
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sean Earle
|
|
@@ -66,7 +66,21 @@ dependencies:
|
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
67
|
version: '0'
|
|
68
68
|
- !ruby/object:Gem::Dependency
|
|
69
|
-
name: tty-
|
|
69
|
+
name: tty-cursor
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: tty-screen
|
|
70
84
|
requirement: !ruby/object:Gem::Requirement
|
|
71
85
|
requirements:
|
|
72
86
|
- - ">="
|
|
@@ -96,6 +110,7 @@ files:
|
|
|
96
110
|
- lib/local_ci/flow.rb
|
|
97
111
|
- lib/local_ci/helper.rb
|
|
98
112
|
- lib/local_ci/job.rb
|
|
113
|
+
- lib/local_ci/output.rb
|
|
99
114
|
- lib/local_ci/rake.rb
|
|
100
115
|
- lib/local_ci/task.rb
|
|
101
116
|
- local_ci.gemspec
|
|
@@ -107,6 +122,7 @@ files:
|
|
|
107
122
|
- spec/unit/local_ci/flow_spec.rb
|
|
108
123
|
- spec/unit/local_ci/helper_spec.rb
|
|
109
124
|
- spec/unit/local_ci/job_spec.rb
|
|
125
|
+
- spec/unit/local_ci/output_spec.rb
|
|
110
126
|
- spec/unit/local_ci/rake_spec.rb
|
|
111
127
|
- spec/unit/local_ci/task_spec.rb
|
|
112
128
|
homepage: https://github.com/HellRok/local_ci
|