local_ci 0.0.5 → 0.0.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 325240cf896966e5fe98d945c9485e3f4b30a32db4762d77f5c3e97664e3f7b1
4
- data.tar.gz: dc2630284356e1ba3a7f22f97107d7c429f423daf2a0e64835b6803e7c935aef
3
+ metadata.gz: c05d333270ce461c92b586bae503e0f2fca0ad8fed179dad1c610062f6627bd6
4
+ data.tar.gz: f63be5d9621379360a8a1cd46dc7f656bdc410f6b6f9639beb87ff11978ee80a
5
5
  SHA512:
6
- metadata.gz: bcbcbe5912235021b56063ebe5b94bf09af69d82aaba2da3b378af1c7c98bf779d684db9eb301c99e231a0af7bf38f5fef69c359bfd82d9aa4b7dc90858851eb
7
- data.tar.gz: b23429e6de91524c432b99951a1e060cfaaa0de32be800591f1020a44f4d1c7aca7db5fd317b2cf6aaccb4517eb59a24c37c73a85a28f6f5e846a88a4fc73443
6
+ metadata.gz: 76685dda1903180053beb224fe5a48e1defaeab1c2bdf247a33bfcaa55342d1aa07f95e123bbb6a8301de9eac1cb174b1683f8bdc8d53a3db33df2494bbabdba
7
+ data.tar.gz: 5eb63c6ba26175a8b8c488388314d887d0c04c7a8621fba2e3ae1cb2ba262a7e686bac8f9eb62de1a9a3718cf7c1ef3c2f69fef70752d72bc96c83d2fd87eab1
data/lib/local_ci/dsl.rb CHANGED
@@ -26,5 +26,13 @@ module LocalCI
26
26
 
27
27
  LocalCI::Flow.new(name: name, heading: heading, parallel: parallel, block: block)
28
28
  end
29
+
30
+ def ci?
31
+ LocalCI::Helper.ci?
32
+ end
33
+
34
+ def local?
35
+ LocalCI::Helper.local?
36
+ end
29
37
  end
30
38
  end
data/lib/local_ci/flow.rb CHANGED
@@ -25,42 +25,64 @@ module LocalCI
25
25
  LocalCI::ExecContext.new(flow: self).instance_exec(&block)
26
26
  end
27
27
 
28
- def actions? = !!@actions
28
+ def actions?
29
+ !!@actions
30
+ end
29
31
 
30
- def actionless? = !actions?
32
+ def actionless?
33
+ !actions?
34
+ end
31
35
 
32
- def isolated? = LocalCI::Task["ci"].already_invoked
36
+ def parallel?
37
+ @parallel
38
+ end
33
39
 
34
- private
40
+ def isolated?
41
+ !LocalCI::Task["ci"].already_invoked
42
+ end
35
43
 
36
- def setup_required_tasks
37
- ci_task = LocalCI::Task["ci", "Run the CI suite"]
44
+ def raise_failures
45
+ LocalCI::Task["ci:teardown"].invoke
46
+ output.failures
38
47
 
39
- LocalCI::Task["ci:setup", "Setup the system to run CI"]
40
- LocalCI::Task["ci:teardown", "Cleanup after the CI"]
48
+ abort LocalCI::Helper.pastel.red("#{@heading} failed, see ci.log for more.")
49
+ end
41
50
 
42
- ci_task.add_prerequisite "ci:setup"
43
- ci_task.define do
44
- LocalCI::Task["ci:teardown"].invoke
45
- end
51
+ def setup_task
52
+ "#{@task}:setup"
53
+ end
54
+
55
+ def jobs_task
56
+ "#{@task}:jobs"
57
+ end
58
+
59
+ def teardown_task
60
+ "#{@task}:teardown"
61
+ end
62
+
63
+ private
64
+
65
+ def setup_required_tasks
66
+ LocalCI::Task["ci"].add_prerequisite "ci:setup"
46
67
  end
47
68
 
48
69
  def setup_flow_tasks
49
- LocalCI::Task["#{@task}:setup"]
70
+ LocalCI::Task[setup_task]
50
71
 
51
- LocalCI::Task.new("#{@task}:jobs", parallel_prerequisites: @parallel)
72
+ LocalCI::Task.new(jobs_task, parallel_prerequisites: @parallel)
52
73
 
53
- LocalCI::Task["#{@task}:teardown"]
74
+ LocalCI::Task[teardown_task]
54
75
  LocalCI::Task[@task, @heading]
55
76
 
56
- LocalCI::Task["#{@task}:setup"]
57
- LocalCI::Task["#{@task}:setup"].add_prerequisite "ci:setup"
77
+ LocalCI::Task[setup_task]
78
+ LocalCI::Task[setup_task].add_prerequisite "ci:setup"
58
79
 
59
- LocalCI::Task[@task].add_prerequisite "#{@task}:teardown"
60
- LocalCI::Task["#{@task}:teardown"].add_prerequisite "#{@task}:jobs"
61
- LocalCI::Task["#{@task}:jobs"].add_prerequisite "#{@task}:setup"
80
+ LocalCI::Task[@task].add_prerequisite jobs_task
81
+ LocalCI::Task[jobs_task].add_prerequisite setup_task
62
82
 
63
83
  LocalCI::Task["ci"].add_prerequisite @task
84
+
85
+ LocalCI.flows << self
64
86
  end
65
87
 
66
88
  def setup_actionless_flow_tasks
@@ -71,15 +93,10 @@ module LocalCI
71
93
 
72
94
  def after_jobs
73
95
  LocalCI::Task[@task].define do
74
- LocalCI::Task["#{@task}:teardown"].invoke
75
- LocalCI::Task["ci:teardown"].invoke unless isolated? || actionless?
76
-
77
- if @failures.any?
78
- LocalCI::Task["ci:teardown"].invoke
79
- output.failures
96
+ LocalCI::Task[teardown_task].invoke
97
+ LocalCI::Task["ci:teardown"].invoke unless !isolated? || actionless?
80
98
 
81
- abort LocalCI::Helper.pastel.red("#{@heading} failed, see ci.log for more.")
82
- end
99
+ raise_failures if @failures.any?
83
100
  end
84
101
  end
85
102
  end
@@ -0,0 +1,41 @@
1
+ module LocalCI
2
+ module Generator
3
+ module Buildkite
4
+ def self.pipeline
5
+ puts steps.to_yaml
6
+ end
7
+
8
+ def self.steps
9
+ {
10
+ "steps" => LocalCI.flows.flat_map do |flow|
11
+ step = {}
12
+
13
+ if flow.parallel?
14
+ step["group"] = flow.heading
15
+ step["steps"] = []
16
+
17
+ flow.jobs.each do |job|
18
+ step["steps"] << {
19
+ "label" => job.name,
20
+ "commands" => [
21
+ "bundle check &> /dev/null || bundle install",
22
+ "bundle exec rake #{job.task}"
23
+ ]
24
+ }
25
+ end
26
+
27
+ else
28
+ step["label"] = flow.heading
29
+ step["commands"] = [
30
+ "bundle check &> /dev/null || bundle install",
31
+ "bundle exec rake #{flow.task}"
32
+ ]
33
+ end
34
+
35
+ [step, "wait"]
36
+ end
37
+ }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,55 @@
1
+ module LocalCI
2
+ module Generator
3
+ module SemaphoreCI
4
+ def self.pipeline
5
+ FileUtils.mkdir_p(".semaphore")
6
+ File.write(".semaphore/semaphore.yml", blocks.to_yaml)
7
+ end
8
+
9
+ def self.blocks
10
+ {
11
+ "version" => "v1.0",
12
+ "name" => "CI",
13
+ "agent" => {
14
+ "machine" => {
15
+ "type" => "f1-standard-2",
16
+ "os_image" => "ubuntu2404"
17
+ }
18
+ },
19
+
20
+ "blocks" => LocalCI.flows.flat_map do |flow|
21
+ block = {
22
+ "name" => flow.heading,
23
+ "task" => {"jobs" => []}
24
+ }
25
+
26
+ if flow.parallel?
27
+ flow.jobs.each do |job|
28
+ block["task"]["jobs"] << {
29
+ "name" => job.name,
30
+ "commands" => [
31
+ "checkout",
32
+ "bundle check &> /dev/null || bundle install",
33
+ "bundle exec rake #{job.task}"
34
+ ]
35
+ }
36
+ end
37
+
38
+ else
39
+ block["task"]["jobs"] << {
40
+ "name" => flow.heading,
41
+ "commands" => [
42
+ "checkout",
43
+ "bundle check &> /dev/null || bundle install",
44
+ "bundle exec rake #{flow.task}"
45
+ ]
46
+ }
47
+ end
48
+
49
+ block
50
+ end
51
+ }
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,11 +1,57 @@
1
1
  module LocalCI
2
2
  module Helper
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"))
3
+ def self.color?
4
+ TTY::Color.support?
5
+ end
6
+
7
+ def self.pastel
8
+ @pastel ||= Pastel.new(enabled: color?)
9
+ end
10
+
11
+ def self.runner
12
+ @runner ||= TTY::Command.new(color: color?, output: logger)
13
+ end
14
+
15
+ def self.logger
16
+ return Logger.new($stdout) if ENV.has_key?("LOCAL_CI_LOG_TO_STDOUT")
17
+
18
+ log_file = ci? ? $stdout : "logs/local_ci.log"
19
+ log_file = ENV.fetch("LOCAL_CI_LOG_FILE", log_file)
20
+
21
+ FileUtils.mkdir_p(File.dirname(log_file)) unless log_file == $stdout
22
+
23
+ Logger.new(log_file)
24
+ end
6
25
 
7
26
  def self.taskize(heading)
8
27
  heading.downcase.gsub(/\s/, "_").gsub(/[^\w]/, "").to_sym
9
28
  end
29
+
30
+ def self.human_duration(time_span)
31
+ seconds = time_span.dup
32
+ minutes = (seconds / 60).to_i
33
+ hours = minutes / 60
34
+
35
+ seconds %= 60
36
+ minutes %= 60
37
+
38
+ if hours >= 1
39
+ "#{hours}h #{minutes}m"
40
+
41
+ elsif minutes >= 1
42
+ "#{minutes}m #{seconds.floor}s"
43
+
44
+ else
45
+ "%.2fs" % seconds
46
+ end
47
+ end
48
+
49
+ def self.ci?
50
+ ENV.has_key?("CI")
51
+ end
52
+
53
+ def self.local?
54
+ !ci?
55
+ end
10
56
  end
11
57
  end
data/lib/local_ci/job.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module LocalCI
2
2
  class Job
3
- attr_reader :flow, :name, :command, :block, :task, :state, :duration
3
+ attr_reader :flow, :name, :command, :block, :task, :state
4
4
 
5
5
  def initialize(flow:, name:, command:, block:)
6
6
  @flow = flow
@@ -18,7 +18,7 @@ module LocalCI
18
18
  ::Rake::Task.define_task(task) do
19
19
  @state = :running
20
20
  @flow.output.update(self)
21
- start = Time.now
21
+ @start = Time.now
22
22
 
23
23
  if block
24
24
  LocalCI::ExecContext.new(flow: @flow).instance_exec(&block)
@@ -32,20 +32,51 @@ module LocalCI
32
32
  job: @name,
33
33
  message: e.message
34
34
  )
35
+
36
+ @flow.raise_failures if isolated?
35
37
  ensure
36
- @duration = Time.now - start
38
+ @duration = duration
37
39
  @flow.output.update(self)
40
+
41
+ if isolated?
42
+ ::Rake::Task[@flow.teardown_task].invoke
43
+ ::Rake::Task["ci:teardown"].invoke
44
+ end
38
45
  end
39
46
 
40
- ::Rake::Task["#{@flow.task}:jobs"].prerequisites << task
47
+ ::Rake::Task[@flow.jobs_task].prerequisites << task
48
+
49
+ ::Rake::Task[task].prerequisites << @flow.setup_task if @flow.actions?
50
+ end
41
51
 
42
- ::Rake::Task[task].prerequisites << "#{@flow.task}:setup" if @flow.actions?
52
+ def isolated?
53
+ !LocalCI::Task[@flow.task].already_invoked
43
54
  end
44
55
 
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
56
+ def duration
57
+ return if @start.nil?
58
+
59
+ @duration || Time.now - @start
60
+ end
61
+
62
+ def waiting?
63
+ @state == :waiting
64
+ end
65
+
66
+ def running?
67
+ @state == :running
68
+ end
69
+
70
+ def success?
71
+ @state == :success
72
+ end
73
+
74
+ def failed?
75
+ @state == :failed
76
+ end
77
+
78
+ def done?
79
+ [:success, :failed].include? @state
80
+ end
50
81
  end
51
82
  end
@@ -24,26 +24,64 @@ module LocalCI
24
24
  @thread.join
25
25
  end
26
26
 
27
- def pastel = LocalCI::Helper.pastel
28
- def cursor = TTY::Cursor
29
- def screen = TTY::Screen
30
- def tty? = $stdout.isatty
27
+ def pastel
28
+ LocalCI::Helper.pastel
29
+ end
30
+
31
+ def cursor
32
+ TTY::Cursor
33
+ end
34
+
35
+ def screen
36
+ TTY::Screen
37
+ end
38
+
39
+ def tty?
40
+ $stdout.isatty
41
+ end
42
+
43
+ def style
44
+ result = LocalCI::Helper.ci? ? "plain" : "realtime"
45
+ result = "plain" unless tty?
46
+ result = ENV.fetch("LOCAL_CI_STYLE", result)
47
+
48
+ unless ["plain", "json", "realtime"].include? result
49
+ raise ArgumentError, "LOCAL_CI_STYLE must be one of plain, json, or realtime"
50
+ end
31
51
 
32
- def passed? = @flow.jobs.all?(&:success?)
33
- def failed? = @flow.failures.any?
34
- def done? = @flow.jobs.all?(&:done?)
52
+ result
53
+ end
54
+
55
+ def passed?
56
+ @flow.jobs.all?(&:success?)
57
+ end
58
+
59
+ def failed?
60
+ @flow.failures.any?
61
+ end
62
+
63
+ def done?
64
+ @flow.jobs.all?(&:done?)
65
+ end
35
66
 
36
67
  def update(job)
37
- if tty?
68
+ case style
69
+ when "realtime"
38
70
  finish and return if done?
39
71
 
40
72
  return if @thread&.alive?
41
73
 
42
74
  start_thread
43
- else
75
+
76
+ when "json"
44
77
  @job = job
45
78
 
46
79
  @mutex.synchronize { json_output }
80
+
81
+ when "plain"
82
+ @job = job
83
+
84
+ @mutex.synchronize { plain_output }
47
85
  end
48
86
  end
49
87
 
@@ -62,6 +100,7 @@ module LocalCI
62
100
  end
63
101
 
64
102
  def draw(final: false)
103
+ print cursor.hide
65
104
  if @first_paint
66
105
  @start = Time.now
67
106
  @first_paint = false
@@ -70,10 +109,12 @@ module LocalCI
70
109
  end
71
110
 
72
111
  puts heading_line
73
- @flow.jobs.each { puts job_line it }
112
+ @flow.jobs.each { |job| puts job_line job }
74
113
  puts footer_line
75
114
 
76
115
  puts if final
116
+ ensure
117
+ print cursor.show
77
118
  end
78
119
 
79
120
  def color(message)
@@ -102,35 +143,24 @@ module LocalCI
102
143
 
103
144
  result = cursor.clear_line
104
145
  if job.waiting?
105
- result << "[ ] #{name}"
146
+ result << "[ ] "
106
147
  elsif job.running?
107
- result << "[-] #{name}"
148
+ result << "[-] "
108
149
  elsif job.success?
109
- result << "[#{pastel.green "✓"}] #{name}"
150
+ result << "[#{pastel.green "✓"}] "
110
151
  elsif job.failed?
111
- result << "[#{pastel.red "✗"}] #{name}"
152
+ result << "[#{pastel.red "✗"}] "
112
153
  end
113
154
 
155
+ result << name
156
+
157
+ result << " (#{pastel.yellow LocalCI::Helper.human_duration(job.duration)})" unless job.waiting?
158
+
114
159
  result
115
160
  end
116
161
 
117
162
  def duration
118
- seconds = Time.now - @start
119
- minutes = (seconds / 60).to_i
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.floor}s"
130
-
131
- else
132
- "%.2fs" % seconds
133
- end
163
+ LocalCI::Helper.human_duration(Time.now - @start)
134
164
  end
135
165
 
136
166
  def footer_line
@@ -146,5 +176,16 @@ module LocalCI
146
176
  state: @job.state
147
177
  }.to_json)
148
178
  end
179
+
180
+ def plain_output
181
+ result = "=== #{@flow.heading} - #{@job.name} [#{@job.state}] "
182
+ result << if @job.waiting? || @job.running?
183
+ "==="
184
+ else
185
+ "(#{LocalCI::Helper.human_duration(@job.duration)}) ==="
186
+ end
187
+
188
+ puts(result)
189
+ end
149
190
  end
150
191
  end
data/lib/local_ci/rake.rb CHANGED
@@ -2,6 +2,23 @@ module LocalCI
2
2
  module Rake
3
3
  def self.setup(klass)
4
4
  klass.send(:include, LocalCI::DSL)
5
+
6
+ ci_task = LocalCI::Task["ci", "Run the CI suite"]
7
+ LocalCI::Task["ci:setup", "Setup the system to run CI"]
8
+ LocalCI::Task["ci:teardown", "Cleanup after the CI"]
9
+
10
+ ci_task.add_prerequisite "ci:setup"
11
+ ci_task.define do
12
+ LocalCI::Task["ci:teardown"].invoke
13
+ end
14
+
15
+ LocalCI::Task["ci:generate:buildkite", "Prints the contents of a Buildkite pipeline.yml the CI suite"] do
16
+ LocalCI::Generator::Buildkite.pipeline
17
+ end
18
+
19
+ LocalCI::Task["ci:generate:semaphore_ci", "Writes a .semaphore/semaphore.yml file for the CI suite"] do
20
+ LocalCI::Generator::SemaphoreCI.pipeline
21
+ end
5
22
  end
6
23
  end
7
24
  end
data/lib/local_ci/task.rb CHANGED
@@ -2,8 +2,10 @@ module LocalCI
2
2
  class Task
3
3
  extend Forwardable
4
4
 
5
- def self.[](task, comment = nil)
6
- new(task, comment: comment)
5
+ def self.[](task, comment = nil, &block)
6
+ new_task = new(task, comment: comment)
7
+ new_task.define(&block) if block_given?
8
+ new_task
7
9
  end
8
10
 
9
11
  attr_accessor :task
data/lib/local_ci.rb CHANGED
@@ -1,6 +1,8 @@
1
+ require "fileutils"
1
2
  require "forwardable"
2
3
  require "json"
3
4
  require "logger"
5
+ require "yaml"
4
6
 
5
7
  require "tty-command"
6
8
  require "tty-cursor"
@@ -16,3 +18,14 @@ require "local_ci/job"
16
18
  require "local_ci/output"
17
19
  require "local_ci/rake"
18
20
  require "local_ci/task"
21
+
22
+ require "local_ci/generator/buildkite"
23
+ require "local_ci/generator/semaphore_ci"
24
+
25
+ module LocalCI
26
+ @flows = []
27
+
28
+ def self.flows
29
+ @flows
30
+ end
31
+ 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.5
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Earle
@@ -9,6 +9,34 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: fileutils
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: json
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
12
40
  - !ruby/object:Gem::Dependency
13
41
  name: logger
14
42
  requirement: !ruby/object:Gem::Requirement
@@ -93,38 +121,38 @@ dependencies:
93
121
  - - ">="
94
122
  - !ruby/object:Gem::Version
95
123
  version: '0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: yaml
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ type: :runtime
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
96
138
  description: A way to run CI locally but also able to run easily on your hosted CI
97
139
  email: sean.r.earle@gmail.com
98
140
  executables: []
99
141
  extensions: []
100
142
  extra_rdoc_files: []
101
143
  files:
102
- - ".gitignore"
103
- - Gemfile
104
- - Gemfile.lock
105
- - Rakefile
106
144
  - lib/local_ci.rb
107
145
  - lib/local_ci/dsl.rb
108
146
  - lib/local_ci/exec_context.rb
109
147
  - lib/local_ci/failure.rb
110
148
  - lib/local_ci/flow.rb
149
+ - lib/local_ci/generator/buildkite.rb
150
+ - lib/local_ci/generator/semaphore_ci.rb
111
151
  - lib/local_ci/helper.rb
112
152
  - lib/local_ci/job.rb
113
153
  - lib/local_ci/output.rb
114
154
  - lib/local_ci/rake.rb
115
155
  - lib/local_ci/task.rb
116
- - local_ci.gemspec
117
- - spec/spec_helper.rb
118
- - spec/support/dsl_klass.rb
119
- - spec/unit/local_ci/dsl_spec.rb
120
- - spec/unit/local_ci/exec_context_spec.rb
121
- - spec/unit/local_ci/failure_spec.rb
122
- - spec/unit/local_ci/flow_spec.rb
123
- - spec/unit/local_ci/helper_spec.rb
124
- - spec/unit/local_ci/job_spec.rb
125
- - spec/unit/local_ci/output_spec.rb
126
- - spec/unit/local_ci/rake_spec.rb
127
- - spec/unit/local_ci/task_spec.rb
128
156
  homepage: https://github.com/HellRok/local_ci
129
157
  licenses:
130
158
  - MIT
@@ -143,7 +171,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
143
171
  - !ruby/object:Gem::Version
144
172
  version: '0'
145
173
  requirements: []
146
- rubygems_version: 3.6.9
174
+ rubygems_version: 3.7.2
147
175
  specification_version: 4
148
176
  summary: Run CI locally
149
177
  test_files: []
data/.gitignore DELETED
@@ -1,2 +0,0 @@
1
- *.log
2
- *.gem