cobra_commander 0.15.1 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +0 -2
- data/.rubocop.yml +0 -5
- data/Gemfile +7 -0
- data/cobra_commander.gemspec +6 -1
- data/doc/dependency_decisions.yml +9 -0
- data/docs/CHANGELOG.md +16 -1
- data/docs/README.md +3 -129
- data/exe/cobra +5 -0
- data/lib/cobra_commander/affected.rb +18 -50
- data/lib/cobra_commander/cli/filters.rb +1 -3
- data/lib/cobra_commander/{output → cli/output}/ascii_tree.rb +7 -5
- data/lib/cobra_commander/cli/output/change.rb +91 -0
- data/lib/cobra_commander/cli/output/dot_graph.rb +21 -0
- data/lib/cobra_commander/cli.rb +58 -30
- data/lib/cobra_commander/component.rb +6 -10
- data/lib/cobra_commander/executor/command.rb +75 -0
- data/lib/cobra_commander/executor/execution.rb +52 -0
- data/lib/cobra_commander/executor/interactive_printer.rb +53 -0
- data/lib/cobra_commander/executor/job.rb +51 -0
- data/lib/cobra_commander/executor/markdown_printer.rb +21 -0
- data/lib/cobra_commander/executor/package_criteria.rb +21 -0
- data/lib/cobra_commander/executor/script.rb +45 -0
- data/lib/cobra_commander/executor/spinners.rb +40 -0
- data/lib/cobra_commander/executor.rb +37 -6
- data/lib/cobra_commander/git_changed.rb +16 -11
- data/lib/cobra_commander/package.rb +21 -0
- data/lib/cobra_commander/registry.rb +29 -0
- data/lib/cobra_commander/source.rb +37 -0
- data/lib/cobra_commander/umbrella.rb +70 -17
- data/lib/cobra_commander/version.rb +1 -1
- data/lib/cobra_commander.rb +10 -16
- data/mkdocs.yml +1 -1
- metadata +37 -43
- data/.editorconfig +0 -11
- data/.github/workflows/ci.yml +0 -44
- data/.github/workflows/release.yml +0 -20
- data/.rubocop_todo.yml +0 -15
- data/LICENSE.txt +0 -21
- data/_config.yml +0 -1
- data/docs/CODE_OF_CONDUCT.md +0 -74
- data/gemfiles/bundler1.gemfile +0 -7
- data/gemfiles/bundler2.gemfile +0 -7
- data/lib/cobra_commander/change.rb +0 -87
- data/lib/cobra_commander/dependencies/bundler/package.rb +0 -17
- data/lib/cobra_commander/dependencies/bundler.rb +0 -44
- data/lib/cobra_commander/dependencies/yarn/package.rb +0 -21
- data/lib/cobra_commander/dependencies/yarn.rb +0 -40
- data/lib/cobra_commander/dependencies.rb +0 -4
- data/lib/cobra_commander/executor/concurrent.rb +0 -51
- data/lib/cobra_commander/executor/context.rb +0 -49
- data/lib/cobra_commander/output/flat_list.rb +0 -16
- data/lib/cobra_commander/output/graph_viz.rb +0 -27
- data/lib/cobra_commander/output/interactive_printer.rb +0 -53
- data/lib/cobra_commander/output/markdown_printer.rb +0 -24
- data/lib/cobra_commander/output.rb +0 -7
- data/portal.yml +0 -12
- data/renovate.json +0 -8
data/lib/cobra_commander/cli.rb
CHANGED
@@ -5,22 +5,21 @@ require "fileutils"
|
|
5
5
|
require "concurrent-ruby"
|
6
6
|
|
7
7
|
require "cobra_commander"
|
8
|
-
require "cobra_commander/affected"
|
9
|
-
require "cobra_commander/change"
|
10
|
-
require "cobra_commander/git_changed"
|
11
|
-
require "cobra_commander/executor"
|
12
|
-
require "cobra_commander/output"
|
13
8
|
|
14
9
|
module CobraCommander
|
15
10
|
# Implements the tool's CLI
|
16
11
|
class CLI < Thor
|
17
|
-
|
12
|
+
require_relative "cli/filters"
|
13
|
+
require_relative "cli/output/ascii_tree"
|
14
|
+
require_relative "cli/output/change"
|
15
|
+
require_relative "cli/output/dot_graph"
|
18
16
|
|
19
17
|
DEFAULT_CONCURRENCY = (Concurrent.processor_count / 2.0).ceil
|
20
18
|
|
21
19
|
class_option :app, default: Dir.pwd, aliases: "-a", type: :string
|
22
|
-
|
23
|
-
|
20
|
+
Source.all.keys.each do |key|
|
21
|
+
class_option key, default: false, type: :boolean, desc: "Consider only the #{key} dependency graph"
|
22
|
+
end
|
24
23
|
|
25
24
|
desc "version", "Prints version"
|
26
25
|
def version
|
@@ -33,11 +32,31 @@ module CobraCommander
|
|
33
32
|
method_option :total, type: :boolean, aliases: "-t", desc: "Prints the total count of components"
|
34
33
|
def ls(components = nil)
|
35
34
|
components = components_filtered(components)
|
36
|
-
puts options.total ? components.size :
|
35
|
+
puts options.total ? components.size : components.map(&:name).sort
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "exec [components] <script>", "Executes the script in the context of a given component or set thereof. " \
|
39
|
+
"Defaults to all components."
|
40
|
+
filter_options dependents: "Run the script on each dependent of a given component",
|
41
|
+
dependencies: "Run the script on each dependency of a given component"
|
42
|
+
method_option :concurrency, type: :numeric, default: DEFAULT_CONCURRENCY, aliases: "-c",
|
43
|
+
desc: "Max number of jobs to run concurrently"
|
44
|
+
method_option :interactive, type: :boolean, default: true, aliases: "-i",
|
45
|
+
desc: "Runs in interactive mode to allow the user to inspect the output of each " \
|
46
|
+
"component"
|
47
|
+
def exec(script_or_components, script = nil)
|
48
|
+
jobs = CobraCommander::Executor::Script.for(
|
49
|
+
components_filtered(script && script_or_components),
|
50
|
+
script || script_or_components
|
51
|
+
)
|
52
|
+
output_mode = options.interactive && jobs.count > 1 ? :interactive : :markdown
|
53
|
+
execution = CobraCommander::Executor.execute(jobs: jobs, workers: options.concurrency,
|
54
|
+
output_mode: output_mode, output: $stdout, status_output: $stderr)
|
55
|
+
exit 1 unless execution.success?
|
37
56
|
end
|
38
57
|
|
39
|
-
desc "
|
40
|
-
|
58
|
+
desc "cmd [components] <command>", "Executes the command in the context of a given component or set thereof. " \
|
59
|
+
"Defaults to all components."
|
41
60
|
filter_options dependents: "Run the command on each dependent of a given component",
|
42
61
|
dependencies: "Run the command on each dependency of a given component"
|
43
62
|
method_option :concurrency, type: :numeric, default: DEFAULT_CONCURRENCY, aliases: "-c",
|
@@ -45,49 +64,58 @@ module CobraCommander
|
|
45
64
|
method_option :interactive, type: :boolean, default: true, aliases: "-i",
|
46
65
|
desc: "Runs in interactive mode to allow the user to inspect the output of each " \
|
47
66
|
"component"
|
48
|
-
def
|
49
|
-
|
50
|
-
|
51
|
-
command
|
52
|
-
concurrency: options.concurrency, status_output: $stderr
|
67
|
+
def cmd(command_or_components, command = nil)
|
68
|
+
jobs = CobraCommander::Executor::Command.for(
|
69
|
+
components_filtered(command && command_or_components),
|
70
|
+
command || command_or_components
|
53
71
|
)
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
end
|
72
|
+
output_mode = options.interactive && jobs.count > 1 ? :interactive : :markdown
|
73
|
+
execution = CobraCommander::Executor.execute(jobs: jobs, workers: options.concurrency,
|
74
|
+
output_mode: output_mode, output: $stdout, status_output: $stderr)
|
75
|
+
exit 1 unless execution.success?
|
59
76
|
end
|
60
77
|
|
61
78
|
desc "tree [component]", "Prints the dependency tree of a given component or umbrella"
|
62
79
|
def tree(component = nil)
|
63
|
-
|
64
|
-
puts
|
80
|
+
components = component ? [find_component(component)] : umbrella.components
|
81
|
+
puts Output::AsciiTree.new(components).to_s
|
65
82
|
end
|
66
83
|
|
67
84
|
desc "graph [component]", "Outputs a graph of a given component or umbrella"
|
68
|
-
method_option :output, default: File.join(Dir.pwd, "output.
|
69
|
-
desc: "Output file, accepts .png or .dot"
|
85
|
+
method_option :output, default: File.join(Dir.pwd, "output.dot"), aliases: "-o"
|
70
86
|
def graph(component = nil)
|
71
|
-
|
72
|
-
|
73
|
-
|
87
|
+
output = File.open(options.output, "w")
|
88
|
+
Output::DotGraph.generate(
|
89
|
+
component ? [find_component(component)] : umbrella.components,
|
90
|
+
output
|
74
91
|
)
|
75
92
|
puts "Graph generated at #{options.output}"
|
76
93
|
rescue ArgumentError => e
|
77
94
|
error e.message
|
95
|
+
ensure
|
96
|
+
output&.close
|
78
97
|
end
|
79
98
|
|
80
99
|
desc "changes [--results=RESULTS] [--branch=BRANCH]", "Prints list of changed files"
|
81
100
|
method_option :results, default: "test", aliases: "-r", desc: "Accepts test, full, name or json"
|
82
101
|
method_option :branch, default: "master", aliases: "-b", desc: "Specified target to calculate against"
|
83
102
|
def changes
|
84
|
-
Change.new(umbrella, options.results, options.branch).run!
|
103
|
+
Output::Change.new(umbrella, options.results, options.branch).run!
|
85
104
|
end
|
86
105
|
|
87
106
|
private
|
88
107
|
|
89
108
|
def umbrella
|
90
|
-
|
109
|
+
selector = Source.all.keys.reduce({}) do |sel, key|
|
110
|
+
sel.merge(key => options.public_send(key))
|
111
|
+
end
|
112
|
+
@umbrella ||= CobraCommander::Umbrella.new(
|
113
|
+
options.app,
|
114
|
+
**selector
|
115
|
+
)
|
116
|
+
rescue ::CobraCommander::Source::Error => e
|
117
|
+
error(e.message)
|
118
|
+
exit(1)
|
91
119
|
end
|
92
120
|
end
|
93
121
|
end
|
@@ -9,22 +9,20 @@ module CobraCommander
|
|
9
9
|
@umbrella = umbrella
|
10
10
|
@name = name
|
11
11
|
@dependency_names = []
|
12
|
-
@packages =
|
12
|
+
@packages = []
|
13
13
|
end
|
14
14
|
|
15
|
-
def add_package(
|
16
|
-
@packages
|
15
|
+
def add_package(package)
|
16
|
+
@packages << package
|
17
17
|
@dependency_names |= package.dependencies
|
18
18
|
end
|
19
19
|
|
20
20
|
def root_paths
|
21
|
-
@packages.
|
22
|
-
File.dirname(package.path)
|
23
|
-
end.uniq
|
21
|
+
@packages.map(&:path).uniq
|
24
22
|
end
|
25
23
|
|
26
24
|
def inspect
|
27
|
-
"#<CobraCommander::Component:#{object_id} #{name} dependencies=#{dependencies.map(&:name)} packages=#{
|
25
|
+
"#<CobraCommander::Component:#{object_id} #{name} dependencies=#{dependencies.map(&:name)} packages=#{packages}>"
|
28
26
|
end
|
29
27
|
|
30
28
|
def deep_dependents
|
@@ -46,9 +44,7 @@ module CobraCommander
|
|
46
44
|
end
|
47
45
|
|
48
46
|
def dependencies
|
49
|
-
@dependencies ||= @dependency_names.sort
|
50
|
-
.map(&@umbrella.method(:find))
|
51
|
-
.compact
|
47
|
+
@dependencies ||= @dependency_names.sort.filter_map { |name| @umbrella.find(name) }
|
52
48
|
end
|
53
49
|
end
|
54
50
|
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./job"
|
4
|
+
require_relative "./package_criteria"
|
5
|
+
|
6
|
+
module CobraCommander
|
7
|
+
module Executor
|
8
|
+
class Command
|
9
|
+
include ::CobraCommander::Executor::Job
|
10
|
+
include ::CobraCommander::Executor::PackageCriteria
|
11
|
+
|
12
|
+
SKIP_UNEXISTING = "Command %s does not exist. Check your cobra.yml for existing commands in %s."
|
13
|
+
SKIP_CRITERIA = "Package %s does not match criteria."
|
14
|
+
|
15
|
+
# Builds the given commands in all packages of all components.
|
16
|
+
#
|
17
|
+
# @param components [Enumerable<CobraCommander::Component>] the target components
|
18
|
+
# @param command [String] shell script to run from the directories of the component's packages
|
19
|
+
# @return [Array<CobraCommander::Executor::Command>]
|
20
|
+
def self.for(components, command)
|
21
|
+
components.flat_map(&:packages).map do |package|
|
22
|
+
new(package, command)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Calls the commands sequentially, stopping ony if an :error happens.
|
27
|
+
#
|
28
|
+
# If one of the commands skips, the result will be :success.
|
29
|
+
#
|
30
|
+
# @param commands [Enumerable<CobraCommander::Component>] the target components
|
31
|
+
# @return [Array<CobraCommander::Executor::Command>]
|
32
|
+
# @see CobraCommander::Executor::Job
|
33
|
+
def self.join(commands)
|
34
|
+
commands.lazy.map(&:call).reduce do |(_, prev_output), (result, output)|
|
35
|
+
new_output = [prev_output&.strip, output&.strip].join("\n")
|
36
|
+
return [:error, new_output] if result == :error
|
37
|
+
|
38
|
+
[:success, new_output]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize(package, command)
|
43
|
+
@package = package
|
44
|
+
@command = command
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s
|
48
|
+
"#{@package.name} (#{@package.key})"
|
49
|
+
end
|
50
|
+
|
51
|
+
# @see CobraCommander::Executor::Job
|
52
|
+
def call
|
53
|
+
command = @package.source.config&.dig("commands", @command)
|
54
|
+
case command
|
55
|
+
when Array then run_multiple(@package, command)
|
56
|
+
when Hash then run_with_criteria(command)
|
57
|
+
when nil then skip(format(SKIP_UNEXISTING, @command, @package.key))
|
58
|
+
else run_script(command, @package.path)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def run_with_criteria(command)
|
65
|
+
return skip(format(SKIP_CRITERIA, @package.name)) unless match_criteria?(@package, command.fetch("if", {}))
|
66
|
+
|
67
|
+
run_script(command["run"], @package.path)
|
68
|
+
end
|
69
|
+
|
70
|
+
def run_multiple(package, commands)
|
71
|
+
Command.join(commands.map { |command| Command.new(package, command) })
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,52 @@
|
|
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
|
@@ -0,0 +1,53 @@
|
|
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
|
@@ -0,0 +1,51 @@
|
|
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
|
@@ -0,0 +1,21 @@
|
|
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
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./job"
|
4
|
+
require_relative "./package_criteria"
|
5
|
+
|
6
|
+
module CobraCommander
|
7
|
+
module Executor
|
8
|
+
module PackageCriteria
|
9
|
+
def match_criteria?(package, criteria)
|
10
|
+
criteria.all? do |criteria_key, criteria_value|
|
11
|
+
criteria_method = "_match_#{criteria_key}?"
|
12
|
+
!respond_to?(criteria_method, true) || send(criteria_method, package, criteria_value)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def _match_depends_on?(package, packages)
|
17
|
+
(Array(packages) - package.dependencies).empty?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./job"
|
4
|
+
|
5
|
+
module CobraCommander
|
6
|
+
module Executor
|
7
|
+
# This is a script job. It can tarket any CobraCommander::Package.
|
8
|
+
#
|
9
|
+
# If you want to target a Component, you can use Script.for to target
|
10
|
+
# individual paths for each given component.
|
11
|
+
#
|
12
|
+
# @see Script.for
|
13
|
+
class Script
|
14
|
+
include ::CobraCommander::Executor::Job
|
15
|
+
|
16
|
+
# Returns a set of scripts to be executed on the given commends.
|
17
|
+
#
|
18
|
+
# If a component has two packages in the same path, only one script for that component will be
|
19
|
+
# returned.
|
20
|
+
#
|
21
|
+
# @param components [Enumerable<CobraCommander::Component>] the target components
|
22
|
+
# @param script [String] shell script to run from the directories of the component's packages
|
23
|
+
# @return [Array<CobraCommander::Executor::Script>]
|
24
|
+
def self.for(components, script)
|
25
|
+
components.flat_map(&:packages).uniq(&:path).map do |package|
|
26
|
+
new(package, script)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(package, script)
|
31
|
+
@package = package
|
32
|
+
@script = script
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
@package.name
|
37
|
+
end
|
38
|
+
|
39
|
+
# @see CobraCommander::Executor::Job
|
40
|
+
def call
|
41
|
+
run_script @script, @package.path
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,40 @@
|
|
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
|
@@ -1,14 +1,45 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "executor/
|
4
|
-
require_relative "executor/
|
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"
|
5
10
|
|
6
11
|
module CobraCommander
|
7
|
-
# Execute a command on all given
|
12
|
+
# Execute a command on all given packages
|
8
13
|
module Executor
|
9
|
-
|
10
|
-
|
11
|
-
|
14
|
+
module_function
|
15
|
+
|
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.
|
20
|
+
#
|
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.
|
25
|
+
#
|
26
|
+
# @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
|
12
43
|
end
|
13
44
|
end
|
14
45
|
end
|
@@ -11,10 +11,11 @@ module CobraCommander
|
|
11
11
|
class GitChanged
|
12
12
|
include Enumerable
|
13
13
|
|
14
|
-
|
14
|
+
Error = Class.new(StandardError)
|
15
|
+
InvalidSelectionError = Class.new(Error)
|
15
16
|
|
16
|
-
def initialize(
|
17
|
-
@
|
17
|
+
def initialize(path, base_branch)
|
18
|
+
@path = path
|
18
19
|
@base_branch = base_branch
|
19
20
|
end
|
20
21
|
|
@@ -25,17 +26,21 @@ module CobraCommander
|
|
25
26
|
private
|
26
27
|
|
27
28
|
def changes
|
28
|
-
@changes ||=
|
29
|
-
|
30
|
-
|
31
|
-
end
|
29
|
+
@changes ||= Dir.chdir(@path) do
|
30
|
+
git_dir, _, result = Open3.capture3("git", "rev-parse", "--git-dir")
|
31
|
+
validate_result!(result)
|
32
32
|
|
33
|
-
|
33
|
+
diff, _, result = Open3.capture3("git", "diff", "--name-only", @base_branch)
|
34
|
+
validate_result!(result)
|
34
35
|
|
35
|
-
|
36
|
-
|
37
|
-
end
|
36
|
+
git_dir = Pathname.new(git_dir)
|
37
|
+
diff.split("\n").map { |f| git_dir.dirname.join(f) }
|
38
38
|
end
|
39
39
|
end
|
40
|
+
|
41
|
+
def validate_result!(result)
|
42
|
+
raise InvalidSelectionError, "Specified branch #{@base_branch} could not be found" if result.exitstatus == 128
|
43
|
+
raise Error, "Uknown git error: #{result.inspect}" if result.exitstatus > 0
|
44
|
+
end
|
40
45
|
end
|
41
46
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module CobraCommander
|
6
|
+
# A package within the
|
7
|
+
class Package
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
attr_reader :source, :path, :name, :dependencies
|
11
|
+
|
12
|
+
def_delegators :source, :key
|
13
|
+
|
14
|
+
def initialize(source, path:, dependencies:, name:)
|
15
|
+
@source = source
|
16
|
+
@path = path
|
17
|
+
@name = name
|
18
|
+
@dependencies = dependencies
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|