cobra_commander 1.0.1 → 1.1.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.
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CobraCommander
4
+ module Executor
5
+ # @private
6
+ #
7
+ # This WorkerPooll will queue up jobs, and execute them using
8
+ # Worker's, each with a thread running our work loop using the
9
+ # given runner.
10
+ #
11
+ # - A *job* is defined by a group of arguments to be passed to the runner.
12
+ # - A *runner* is an object that respond to #call(tty, *args), where TTY is
13
+ # an instance of TTY::Command, and *args are the arguments queued in the
14
+ # worker pool.
15
+ # - The *runner* call method must return an array of [status, output]
16
+ # - A worker manages a thread running the job wirh runner.call and updates
17
+ # the job result and output.
18
+ #
19
+ class WorkerPool
20
+ Job = Struct.new(:name, :args) do
21
+ attr_reader :output, :status
22
+
23
+ def resolve!(status, output)
24
+ @status = status
25
+ @output = output
26
+ end
27
+
28
+ def resolved?
29
+ !@status.nil?
30
+ end
31
+ end
32
+
33
+ class Worker
34
+ attr_reader :id
35
+
36
+ def initialize(id, pool)
37
+ @id = id
38
+ @pool = pool
39
+ end
40
+
41
+ def kill
42
+ @thread&.kill
43
+ end
44
+
45
+ def spawn
46
+ @thread = Thread.new do
47
+ loop { break if @pool.run_next == :exit }
48
+ end
49
+ end
50
+
51
+ def wait
52
+ @thread.join
53
+ end
54
+ end
55
+
56
+ attr_reader :jobs
57
+
58
+ def initialize(runner:, workers:, printer:, jobs:, &name_f)
59
+ @tty = ::CobraCommander::Executor::IsolatedPTY.new(printer: printer)
60
+ @runner = runner
61
+ @workers = Array.new(workers) { |id| Worker.new(id, self) }
62
+ @jobs = []
63
+ @queue = Queue.new
64
+
65
+ push_all(jobs, &(name_f || :describe))
66
+ end
67
+
68
+ def error?
69
+ jobs.map(&:status).any?(:error)
70
+ end
71
+
72
+ def push_all(jobs, &name_f)
73
+ jobs.each { push(name_f&.(_1), *Array(_1)) }
74
+ end
75
+
76
+ def push(name, *args)
77
+ Job.new(name, args).tap do |job|
78
+ @jobs.push(job)
79
+ @queue.push(job)
80
+ end
81
+ end
82
+
83
+ def start
84
+ @stop = false
85
+ @workers.each(&:spawn)
86
+ @queue.close
87
+ begin
88
+ @workers.each(&:wait)
89
+ rescue Interrupt
90
+ @workers.each(&:kill) if @stop
91
+ @stop = true
92
+ retry
93
+ end
94
+ end
95
+
96
+ def run_next
97
+ return :exit if @stop
98
+ return :exit unless (job = @queue.pop)
99
+
100
+ job.resolve!(*@runner.call(@tty, *job.args))
101
+ end
102
+ end
103
+ end
104
+ end
@@ -1,45 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "executor/execution"
4
- require_relative "executor/job"
5
- require_relative "executor/script"
6
- require_relative "executor/command"
7
- require_relative "executor/spinners"
8
- require_relative "executor/interactive_printer"
9
- require_relative "executor/markdown_printer"
3
+ require "pastel"
4
+ require "tty-command"
5
+ require "tty-prompt"
10
6
 
11
7
  module CobraCommander
12
8
  # Execute a command on all given packages
13
9
  module Executor
10
+ autoload :BufferedPrinter, "cobra_commander/executor/buffered_printer"
11
+ autoload :Command, "cobra_commander/executor/command"
12
+ autoload :OutputPrompt, "cobra_commander/executor/output_prompt"
13
+ autoload :IsolatedPTY, "cobra_commander/executor/isolated_pty"
14
+ autoload :PackageCriteria, "cobra_commander/executor/package_criteria"
15
+ autoload :Printers, "cobra_commander/executor/printers"
16
+ autoload :RunScript, "cobra_commander/executor/run_script"
17
+ autoload :Script, "cobra_commander/executor/script"
18
+ autoload :WorkerPool, "cobra_commander/executor/worker_pool"
19
+
14
20
  module_function
15
21
 
16
- # Executes the given jobs in an CobraCommander::Executor::Execution.
17
- #
18
- # This facade also allows to execute the jobs with a spinner (@see CobraCommander::Executor::Spinners) to display
19
- # the execution status of each job.
22
+ # Executes the given jobs in an CobraCommander::Executor::WorkerPool.
20
23
  #
21
- # You can also determine how to display the execution once it's completed, by setting `output_mode` to either
22
- # :interactive or :markdown. When using :interactive, a menu with each job will be displayed allowing the user
23
- # to select a job and see its output. When using :markdown, a markdown will be printed to `output` with the
24
- # output of each job.
24
+ # When only one job is queued, it choses the :quiet printer, to print the
25
+ # output as it happens.
26
+ # When more than one job is queued in interactive mode, it uses the :progress
27
+ # printer, which will display a green dot or a red F depending on the result
28
+ # of each script execution.
29
+ # If not in interactive mode, it will print the output in a buffered way to
30
+ # make it easier to read each output.
25
31
  #
26
32
  # @param jobs [Enumerable<CobraCommander::Executor::Job>] the jobs to run
27
- # @param status_output [IO,nil] if not nil, will print the spinners for each job in this output
28
- # @param workers [Integer] number of workers processing the jobs queue (see CobraCommander::Executor::Execution)
29
- # @param output_mode [:interactive,:markdown,nil] how the output will be printed after execution
30
- # @param workers [Integer] number of workers processing the jobs queue (see CobraCommander::Executor::Execution)
31
- # @return [CobraCommander::Executor::Execution]
32
- # @see CobraCommander::Executor::Execution
33
- # @see CobraCommander::Executor::Spinners
34
- # @see CobraCommander::Executor::InterativePrinter
35
- # @see CobraCommander::Executor::MarkdownPrinter
36
- def execute(jobs:, status_output: nil, output_mode: nil, output: nil, **kwargs)
37
- Execution.new(jobs, **kwargs).tap do |execution|
38
- Spinners.start(execution, output: status_output) if status_output
39
- execution.wait
40
- InteractivePrinter.run(execution, output) if output_mode == :interactive
41
- MarkdownPrinter.run(execution, output) if output_mode == :markdown
42
- end
33
+ # @param interactive [Boolean] prefer interactive output
34
+ # @see CobraCommander::Executor::WorkerPool for more options
35
+ def execute_and_handle_exit(jobs:, interactive: false, **kwargs, &name_f)
36
+ printer = if jobs.size == 1 then :quiet
37
+ elsif interactive then :progress
38
+ else
39
+ ::CobraCommander::Executor::BufferedPrinter
40
+ end
41
+ pool = WorkerPool.new(jobs: jobs, printer: printer, **kwargs, &name_f).tap(&:start)
42
+ return CobraCommander::Executor::OutputPrompt.run(pool) if interactive && jobs.size > 1
43
+
44
+ exit(1) if pool.error?
43
45
  end
44
46
  end
45
47
  end
@@ -17,5 +17,9 @@ module CobraCommander
17
17
  @name = name
18
18
  @dependencies = dependencies
19
19
  end
20
+
21
+ def describe
22
+ "#{name} (#{key})"
23
+ end
20
24
  end
21
25
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CobraCommander
4
- VERSION = "1.0.1"
4
+ VERSION = "1.1.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cobra_commander
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Langfeld
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2023-01-05 00:00:00.000000000 Z
13
+ date: 2023-03-09 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: bundler
@@ -26,20 +26,6 @@ dependencies:
26
26
  - - ">="
27
27
  - !ruby/object:Gem::Version
28
28
  version: '0'
29
- - !ruby/object:Gem::Dependency
30
- name: concurrent-ruby
31
- requirement: !ruby/object:Gem::Requirement
32
- requirements:
33
- - - "~>"
34
- - !ruby/object:Gem::Version
35
- version: '1.1'
36
- type: :runtime
37
- prerelease: false
38
- version_requirements: !ruby/object:Gem::Requirement
39
- requirements:
40
- - - "~>"
41
- - !ruby/object:Gem::Version
42
- version: '1.1'
43
29
  - !ruby/object:Gem::Dependency
44
30
  name: thor
45
31
  requirement: !ruby/object:Gem::Requirement
@@ -88,20 +74,6 @@ dependencies:
88
74
  - - "~>"
89
75
  - !ruby/object:Gem::Version
90
76
  version: 0.23.1
91
- - !ruby/object:Gem::Dependency
92
- name: tty-spinner
93
- requirement: !ruby/object:Gem::Requirement
94
- requirements:
95
- - - "~>"
96
- - !ruby/object:Gem::Version
97
- version: 0.9.3
98
- type: :runtime
99
- prerelease: false
100
- version_requirements: !ruby/object:Gem::Requirement
101
- requirements:
102
- - - "~>"
103
- - !ruby/object:Gem::Version
104
- version: 0.9.3
105
77
  - !ruby/object:Gem::Dependency
106
78
  name: aruba
107
79
  requirement: !ruby/object:Gem::Requirement
@@ -241,16 +213,7 @@ executables:
241
213
  extensions: []
242
214
  extra_rdoc_files: []
243
215
  files:
244
- - ".gitignore"
245
- - ".rspec"
246
- - ".rubocop.yml"
247
- - Gemfile
248
- - Guardfile
249
- - Rakefile
250
- - bin/console
251
- - bin/setup
252
216
  - cobra_commander.gemspec
253
- - doc/dependency_decisions.yml
254
217
  - docs/CHANGELOG.md
255
218
  - docs/README.md
256
219
  - exe/cobra
@@ -263,21 +226,20 @@ files:
263
226
  - lib/cobra_commander/cli/output/dot_graph.rb
264
227
  - lib/cobra_commander/component.rb
265
228
  - lib/cobra_commander/executor.rb
229
+ - lib/cobra_commander/executor/buffered_printer.rb
266
230
  - lib/cobra_commander/executor/command.rb
267
- - lib/cobra_commander/executor/execution.rb
268
- - lib/cobra_commander/executor/interactive_printer.rb
269
- - lib/cobra_commander/executor/job.rb
270
- - lib/cobra_commander/executor/markdown_printer.rb
231
+ - lib/cobra_commander/executor/isolated_pty.rb
232
+ - lib/cobra_commander/executor/output_prompt.rb
271
233
  - lib/cobra_commander/executor/package_criteria.rb
234
+ - lib/cobra_commander/executor/run_script.rb
272
235
  - lib/cobra_commander/executor/script.rb
273
- - lib/cobra_commander/executor/spinners.rb
236
+ - lib/cobra_commander/executor/worker_pool.rb
274
237
  - lib/cobra_commander/git_changed.rb
275
238
  - lib/cobra_commander/package.rb
276
239
  - lib/cobra_commander/registry.rb
277
240
  - lib/cobra_commander/source.rb
278
241
  - lib/cobra_commander/umbrella.rb
279
242
  - lib/cobra_commander/version.rb
280
- - mkdocs.yml
281
243
  homepage: http://tech.powerhrg.com/cobra_commander/
282
244
  licenses:
283
245
  - MIT
@@ -301,7 +263,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
301
263
  - !ruby/object:Gem::Version
302
264
  version: '0'
303
265
  requirements: []
304
- rubygems_version: 3.4.1
266
+ rubygems_version: 3.4.6
305
267
  signing_key:
306
268
  specification_version: 4
307
269
  summary: Tools for working with Component Based Rails Apps
data/.gitignore DELETED
@@ -1,16 +0,0 @@
1
- /.bundle/
2
- /.DS_store
3
- /.yardoc
4
- /Gemfile.lock
5
- /_yardoc/
6
- /coverage/
7
- /graph.*
8
- /pkg/
9
- /spec/examples.txt
10
- /spec/reports/
11
- /spec/fixtures/app/node_modules/
12
- /spec/fixtures/app/node_manifest/node_modules/
13
- /spec/fixtures/app/components/*/node_modules/
14
- /tmp/
15
-
16
- .bundle
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper
data/.rubocop.yml DELETED
@@ -1,8 +0,0 @@
1
- require:
2
- - rubocop-powerhome
3
-
4
- Rails:
5
- Enabled: false
6
-
7
- Style/ClassAndModuleChildren:
8
- Enabled: false
data/Gemfile DELETED
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source "https://rubygems.org"
4
-
5
- gemspec
6
-
7
- bundler_version = ENV.fetch("BUNDLER_VERSION", "2")
8
- gem "bundler", "~> #{bundler_version}"
9
-
10
- group :cobra do
11
- gem "cobra_commander-stub", path: "./spec/support/", require: "stub_source"
12
- end
data/Guardfile DELETED
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- guard :rspec, cmd: "bundle exec rspec" do
4
- require "guard/rspec/dsl"
5
- dsl = Guard::RSpec::Dsl.new(self)
6
-
7
- rspec = dsl.rspec
8
- watch(rspec.spec_helper) { rspec.spec_dir }
9
- watch(rspec.spec_support) { rspec.spec_dir }
10
- watch(rspec.spec_files)
11
-
12
- ruby = dsl.ruby
13
- dsl.watch_spec_files_for(ruby.lib_files)
14
- end
data/Rakefile DELETED
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "rubocop/rake_task"
5
- require "rspec/core/rake_task"
6
-
7
- RSpec::Core::RakeTask.new(:spec)
8
- RuboCop::RakeTask.new(:rubocop)
9
-
10
- task default: %i[spec rubocop]
data/bin/console DELETED
@@ -1,15 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require "bundler/setup"
5
- require "cobra_commander"
6
-
7
- # You can add fixtures and/or initialization code here to make experimenting
8
- # with your gem easier. You can also use a different console, if you like.
9
-
10
- # (If you use this, don't forget to add pry to your Gemfile!)
11
- # require "pry"
12
- # Pry.start
13
-
14
- require "irb"
15
- IRB.start
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here
@@ -1,9 +0,0 @@
1
- ---
2
- - - :inherit_from
3
- - https://raw.githubusercontent.com/powerhome/oss-guide/master/license_rules.yml
4
- - - :approve
5
- - bundler
6
- - :who: Carlos Palhares <carlos.palhares@powerhrg.com>
7
- :why: 'Bundler <https://rubygems.org/gems/bundler/versions/1.17.3> is licensed under MIT, but license_finder sometimes fails to resolve that with 1.17.3.'
8
- :versions: [1.17.3]
9
- :when: 2022-11-02 20:36:55.617698000 Z
@@ -1,52 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "concurrent-ruby"
4
-
5
- module CobraCommander
6
- module Executor
7
- # A threaded execution environment, limited by the number of given workers
8
- class Execution < Hash
9
- #
10
- # @param jobs [Array<#call>] array of job objects
11
- # @param workers [Integer] number of workers to process this execution
12
- # @see CobraCommander::Executor::Job
13
- def initialize(jobs, workers:)
14
- super()
15
-
16
- @executor = Concurrent::FixedThreadPool.new(workers, auto_terminate: true)
17
- merge! create_futures(jobs)
18
- end
19
-
20
- # Wait for all jobs to complete, returns a future with all execution futures
21
- # @return [Concurrent::Promises::Future]
22
- def wait
23
- Concurrent::Promises.zip_futures_on(@executor, *values)
24
- .tap(&:wait)
25
- end
26
-
27
- # The execution succeeds when all jobs succeeded
28
- # @return [Boolean]
29
- def success?
30
- values.all?(&:fulfilled?)
31
- end
32
-
33
- private
34
-
35
- def create_future(job)
36
- Concurrent::Promises.future_on(@executor, job, &:call).then do |result|
37
- status, output = result
38
- case status
39
- when :error then Concurrent::Promises.rejected_future(output)
40
- when :success, :skip then Concurrent::Promises.fulfilled_future(output)
41
- else
42
- Concurrent::Promises.fulfilled_future(result)
43
- end
44
- end.flat
45
- end
46
-
47
- def create_futures(jobs)
48
- jobs.to_h { |job| [job, create_future(job)] }
49
- end
50
- end
51
- end
52
- end
@@ -1,53 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "pastel"
4
- require "tty-prompt"
5
-
6
- module CobraCommander
7
- module Executor
8
- # Runs an interactive output printer
9
- class InteractivePrinter
10
- pastel = Pastel.new
11
- SUCCESS = "#{pastel.green('✔')} %s"
12
- ERROR = "#{pastel.red('✖')} %s"
13
- BYE = pastel.decorate("\n\n👋 Bye!", :white, :on_black, :bold).freeze
14
-
15
- def self.run(execution, output)
16
- new(execution).run(output)
17
- end
18
-
19
- def initialize(execution)
20
- @prompt = TTY::Prompt.new
21
- @execution = execution
22
- end
23
-
24
- def run(output)
25
- selected = nil
26
- loop do
27
- selected = @prompt.select("Print output?", options, default: options.key(selected))
28
- output.puts selected.fulfilled? ? selected.value : selected.reason
29
- end
30
- rescue TTY::Reader::InputInterrupt
31
- output.puts BYE
32
- end
33
-
34
- private
35
-
36
- def options
37
- @options ||= @execution.sort { |*args| sort_results(*args.flatten) }
38
- .reduce({}) do |options, (job, result)|
39
- template = result.rejected? ? ERROR : SUCCESS
40
- options.merge format(template, job.to_s) => result
41
- end
42
- end
43
-
44
- def sort_results(job_a, result_a, job_b, result_b)
45
- if result_a.rejected? == result_b.rejected?
46
- job_a.to_s <=> job_b.to_s
47
- else
48
- result_b.rejected? ? 1 : -1
49
- end
50
- end
51
- end
52
- end
53
- end
@@ -1,51 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "tty-command"
4
-
5
- module CobraCommander
6
- module Executor
7
- #
8
- # This is a helper module to help jobs return valid errors and success outputs.
9
- #
10
- # A CobraCommander::Executor::Job is actually any object responding to #call, and returning
11
- # valid error or success responses.
12
- #
13
- # An error is an array containint `:error` and the output (i.e.: [:error, "string output"]).
14
- # A success is either an array containint `:success` and the output, or just the output
15
- # (i.e.: [:error, "string output"] or just "string output").
16
- #
17
- module Job
18
- def skip(reason)
19
- [:skip, reason]
20
- end
21
-
22
- def error(output)
23
- [:error, output]
24
- end
25
-
26
- def success(output)
27
- [:success, output]
28
- end
29
-
30
- def run_script(script, path)
31
- result = isolate_bundle do
32
- TTY::Command.new(pty: true, printer: :null)
33
- .run!(script, chdir: path, err: :out)
34
- end
35
- return error(result.out) if result.failed?
36
-
37
- success(result.out)
38
- end
39
-
40
- private
41
-
42
- def isolate_bundle(&block)
43
- if Bundler.respond_to?(:with_unbundled_env)
44
- Bundler.with_unbundled_env(&block)
45
- else
46
- Bundler.with_clean_env(&block)
47
- end
48
- end
49
- end
50
- end
51
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module CobraCommander
4
- module Executor
5
- # Prints the given CobraCommander::Executor::Context to [output] collection in markdown
6
- module MarkdownPrinter
7
- SUCCESS = "\n## ✔ %s\n"
8
- ERROR = "\n## ✖ %s\n"
9
- OUTPUT = "\n```\n\n%s\n```\n"
10
-
11
- def self.run(execution, output)
12
- execution.each do |job, result|
13
- template = result.fulfilled? ? SUCCESS : ERROR
14
-
15
- output.print format(template, job)
16
- output.print format(OUTPUT, result.fulfilled? ? result.value : result.reason)
17
- end
18
- end
19
- end
20
- end
21
- end
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "pastel"
4
- require "tty-spinner"
5
-
6
- module CobraCommander
7
- module Executor
8
- class Spinners
9
- pastel = ::Pastel.new
10
- SPINNER_OPTIONS = {
11
- format: :bouncing,
12
- success_mark: pastel.green("[DONE]"),
13
- error_mark: pastel.red("[ERROR]"),
14
- }.freeze
15
-
16
- def self.start(execution, output:)
17
- new(execution, output: output).start
18
- end
19
-
20
- def initialize(execution, output:)
21
- @multi = TTY::Spinner::Multi.new(":spinner :task", output: output)
22
- @multi.top_spinner.update(task: "Running")
23
- execution.each { |job, result| register_spinner(job, result) }
24
- end
25
-
26
- def start
27
- @multi.auto_spin
28
- end
29
-
30
- private
31
-
32
- def register_spinner(job, result)
33
- @multi.register(":spinner #{job}", **SPINNER_OPTIONS) do |spinner|
34
- result.on_fulfillment!(spinner) { |_, spin| spin.success }
35
- .on_rejection!(spinner) { |_, spin| spin.error }
36
- end
37
- end
38
- end
39
- end
40
- end
data/mkdocs.yml DELETED
@@ -1,8 +0,0 @@
1
- site_name: Cobra Commander
2
- site_description: Cobra Commander Documentation
3
- repo_url: https://github.com/powerhome/cobra_commander
4
- nav:
5
- - "Home": "README.md"
6
- - "Changelog": "CHANGELOG.md"
7
- plugins:
8
- - techdocs-core