cobra_commander 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/cobra_commander.gemspec +15 -20
- data/docs/CHANGELOG.md +10 -0
- data/lib/cobra_commander/affected.rb +6 -41
- data/lib/cobra_commander/cli/filters.rb +1 -1
- data/lib/cobra_commander/cli/output/ascii_tree.rb +2 -2
- data/lib/cobra_commander/cli/output/change.rb +12 -52
- data/lib/cobra_commander/cli.rb +21 -30
- data/lib/cobra_commander/component.rb +29 -0
- data/lib/cobra_commander/executor/buffered_printer.rb +41 -0
- data/lib/cobra_commander/executor/command.rb +22 -45
- data/lib/cobra_commander/executor/isolated_pty.rb +20 -0
- data/lib/cobra_commander/executor/output_prompt.rb +59 -0
- data/lib/cobra_commander/executor/package_criteria.rb +0 -3
- data/lib/cobra_commander/executor/run_script.rb +25 -0
- data/lib/cobra_commander/executor/script.rb +16 -30
- data/lib/cobra_commander/executor/worker_pool.rb +104 -0
- data/lib/cobra_commander/executor.rb +33 -31
- data/lib/cobra_commander/git_changed.rb +2 -2
- data/lib/cobra_commander/package.rb +5 -1
- data/lib/cobra_commander/source.rb +13 -2
- data/lib/cobra_commander/version.rb +1 -1
- metadata +48 -89
- data/.gitignore +0 -16
- data/.rspec +0 -3
- data/.rubocop.yml +0 -8
- data/Gemfile +0 -12
- data/Guardfile +0 -14
- data/Rakefile +0 -10
- data/bin/console +0 -15
- data/bin/setup +0 -8
- data/doc/dependency_decisions.yml +0 -9
- data/lib/cobra_commander/executor/execution.rb +0 -52
- data/lib/cobra_commander/executor/interactive_printer.rb +0 -53
- data/lib/cobra_commander/executor/job.rb +0 -51
- data/lib/cobra_commander/executor/markdown_printer.rb +0 -21
- data/lib/cobra_commander/executor/spinners.rb +0 -40
- data/mkdocs.yml +0 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: da1b8808311f316b8e200895575b653357a88b92f8a1d05d833ab8f0bd55b0d5
|
|
4
|
+
data.tar.gz: 442d8ae693c4bac9f3d888a33fc4b5e1d1a5904c681dba43144284651b42abfd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '07940522fad6ad8f3220e25f654a19a34a8b61497b0f68c33c368db9926fd8f32446e4423112f91630511c447469866398b152736db79fc726e27dd96809d0c4'
|
|
7
|
+
data.tar.gz: a1e5b9db402504b0114c7cad3a65a4ba22bce8ba003269abb03a824128df5d586c1c39fde4d9132da97d71143b2b3d65bb6966dba464a8068e5193fb159aa373
|
data/cobra_commander.gemspec
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
|
-
require "cobra_commander/version"
|
|
3
|
+
require_relative "lib/cobra_commander/version"
|
|
6
4
|
|
|
7
5
|
Gem::Specification.new do |spec|
|
|
8
6
|
spec.name = "cobra_commander"
|
|
@@ -25,33 +23,30 @@ Gem::Specification.new do |spec|
|
|
|
25
23
|
DESCRIPTION
|
|
26
24
|
spec.homepage = "http://tech.powerhrg.com/cobra_commander/"
|
|
27
25
|
spec.license = "MIT"
|
|
26
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
28
27
|
|
|
29
28
|
spec.metadata["rubygems_mfa_required"] = "true"
|
|
30
29
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
31
30
|
spec.metadata["source_code_uri"] = "https://github.com/powerhome/cobra_commander"
|
|
32
31
|
spec.metadata["changelog_uri"] = "https://github.com/powerhome/cobra_commander/blob/main/cobra_commander/docs/CHANGELOG.md"
|
|
33
32
|
|
|
34
|
-
spec.files =
|
|
35
|
-
f.match(%r{^(test|spec|features)/})
|
|
36
|
-
end
|
|
33
|
+
spec.files = Dir["{docs,exe,lib}/**/*"] + ["cobra_commander.gemspec"]
|
|
37
34
|
spec.bindir = "exe"
|
|
38
|
-
spec.executables =
|
|
39
|
-
spec.require_paths = [
|
|
35
|
+
spec.executables = %w[cobra]
|
|
36
|
+
spec.require_paths = %w[lib]
|
|
40
37
|
|
|
41
|
-
spec.add_dependency "bundler"
|
|
42
|
-
spec.add_dependency "concurrent-ruby", "~> 1.1"
|
|
38
|
+
spec.add_dependency "bundler", ">= 2.4.17"
|
|
43
39
|
spec.add_dependency "thor", ["< 2.0", ">= 0.18.1"]
|
|
44
40
|
spec.add_dependency "tty-command", "~> 0.10.0"
|
|
45
41
|
spec.add_dependency "tty-prompt", "~> 0.23.1"
|
|
46
|
-
spec.add_dependency "tty-spinner", "~> 0.9.3"
|
|
47
42
|
|
|
48
|
-
spec.add_development_dependency "aruba", "
|
|
49
|
-
spec.add_development_dependency "
|
|
50
|
-
spec.add_development_dependency "
|
|
51
|
-
spec.add_development_dependency "
|
|
52
|
-
spec.add_development_dependency "pry"
|
|
53
|
-
spec.add_development_dependency "rake", "
|
|
54
|
-
spec.add_development_dependency "rspec", "
|
|
55
|
-
spec.add_development_dependency "rubocop", "1.
|
|
56
|
-
spec.add_development_dependency "rubocop-powerhome", "
|
|
43
|
+
spec.add_development_dependency "aruba", "0.14.14"
|
|
44
|
+
spec.add_development_dependency "guard-rspec", "4.7.3"
|
|
45
|
+
spec.add_development_dependency "license_finder", "7.1"
|
|
46
|
+
spec.add_development_dependency "ostruct", "0.6.3"
|
|
47
|
+
spec.add_development_dependency "pry", "0.14.2"
|
|
48
|
+
spec.add_development_dependency "rake", "13.0.6"
|
|
49
|
+
spec.add_development_dependency "rspec", "3.13.0"
|
|
50
|
+
spec.add_development_dependency "rubocop", "1.82.1"
|
|
51
|
+
spec.add_development_dependency "rubocop-powerhome", "0.6.1"
|
|
57
52
|
end
|
data/docs/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## Version 1.2.0 - 2026-06-09
|
|
4
|
+
|
|
5
|
+
* Add an `around_command` extension point so plugins can enhance the shell used to run `cmd`/`exec`. `Source#around_command` wraps execution (e.g. Bundler isolation) and yields a hash of environment variables for the command (base yields `{}`, scoped per-execution via TTY). Packages delegate to their source; `Component#around_command` nests each distinct source's wrapper and yields their merged env. `exec` applies it once per component (all packages contribute, later sources win); `cmd` applies each package's own. `IsolatedPTY` is now a plain PTY command — isolation comes from the sources.
|
|
6
|
+
* Raise the minimum supported Ruby version to 3.2 and expand CI to cover Ruby 4.0.
|
|
7
|
+
|
|
8
|
+
## Version 1.1.0 - 2023-03-09
|
|
9
|
+
|
|
10
|
+
* New Executor by @xjunior in [#104](https://github.com/powerhome/cobra_commander/pull/104)
|
|
11
|
+
* Cleanup cobra changes by @xjunior in [#105](https://github.com/powerhome/cobra_commander/pull/105)
|
|
12
|
+
|
|
3
13
|
## Version 1.0.1 - 2023-01-05
|
|
4
14
|
|
|
5
15
|
* Fix Umbrella#resolve unable to resolve a path relative to the project @xjunior [#103](https://github.com/powerhome/cobra_commander/pull/103)
|
|
@@ -1,25 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "json"
|
|
4
|
-
|
|
5
3
|
module CobraCommander
|
|
6
4
|
# Calculates directly & transitively affected components
|
|
5
|
+
# Takes a list of changes, and resolves all components affected
|
|
6
|
+
#
|
|
7
7
|
class Affected
|
|
8
|
+
include Enumerable
|
|
9
|
+
|
|
8
10
|
def initialize(umbrella, changes)
|
|
9
11
|
@umbrella = umbrella
|
|
10
12
|
@changes = changes
|
|
11
13
|
end
|
|
12
14
|
|
|
13
|
-
def
|
|
14
|
-
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def all
|
|
18
|
-
@all ||= (directly | transitively).sort_by(&:name)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def scripts
|
|
22
|
-
@scripts ||= paths.map { |path| path.join("test.sh") }
|
|
15
|
+
def each(&)
|
|
16
|
+
(directly | transitively).sort_by(&:name).each(&)
|
|
23
17
|
end
|
|
24
18
|
|
|
25
19
|
def directly
|
|
@@ -31,34 +25,5 @@ module CobraCommander
|
|
|
31
25
|
@transitively ||= directly.flat_map(&:deep_dependents)
|
|
32
26
|
.uniq.sort_by(&:name)
|
|
33
27
|
end
|
|
34
|
-
|
|
35
|
-
def to_json(*_args)
|
|
36
|
-
{
|
|
37
|
-
changed_files: @changes,
|
|
38
|
-
directly_affected_components: directly.map { |c| affected_component(c) },
|
|
39
|
-
transitively_affected_components: transitively.map { |c| affected_component(c) },
|
|
40
|
-
test_scripts: scripts,
|
|
41
|
-
component_names: names,
|
|
42
|
-
languages: all_affected_packages,
|
|
43
|
-
}.to_json
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
private
|
|
47
|
-
|
|
48
|
-
def affected_component(component)
|
|
49
|
-
{
|
|
50
|
-
name: component.name,
|
|
51
|
-
path: component.root_paths.map(&:to_s),
|
|
52
|
-
type: component.packages.map(&:key).map(&:to_s),
|
|
53
|
-
}
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def paths
|
|
57
|
-
@paths ||= all.map(&:root_paths).flatten
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def all_affected_packages
|
|
61
|
-
all.flat_map(&:packages).map(&:key).uniq
|
|
62
|
-
end
|
|
63
28
|
end
|
|
64
29
|
end
|
|
@@ -43,12 +43,12 @@ module CobraCommander
|
|
|
43
43
|
|
|
44
44
|
def add_tee(io, outdents, dep)
|
|
45
45
|
io.puts line(outdents, TEE, dep.name)
|
|
46
|
-
list_dependencies(io, dep,
|
|
46
|
+
list_dependencies(io, dep, outdents + [BAR])
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def add_corner(io, outdents, dep)
|
|
50
50
|
io.puts line(outdents, CORNER, dep.name)
|
|
51
|
-
list_dependencies(io, dep,
|
|
51
|
+
list_dependencies(io, dep, outdents + [SPACE])
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def line(outdents, sym, name)
|
|
@@ -6,27 +6,19 @@ require "cobra_commander/affected"
|
|
|
6
6
|
module CobraCommander
|
|
7
7
|
class CLI
|
|
8
8
|
module Output
|
|
9
|
-
# Calculates and prints affected components & files
|
|
10
9
|
class Change
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def initialize(umbrella, oformat, branch, changes: nil)
|
|
14
|
-
@format = oformat
|
|
10
|
+
def initialize(umbrella, branch, changes: nil)
|
|
15
11
|
@branch = branch
|
|
16
12
|
@umbrella = umbrella
|
|
17
13
|
@changes = changes || GitChanged.new(umbrella.path, branch)
|
|
18
14
|
end
|
|
19
15
|
|
|
20
16
|
def run!
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
tests_to_run
|
|
27
|
-
end
|
|
28
|
-
rescue GitChanged::InvalidSelectionError => e
|
|
29
|
-
puts e.message
|
|
17
|
+
print_changes_since_last_commit
|
|
18
|
+
puts
|
|
19
|
+
print_directly_affected_components
|
|
20
|
+
puts
|
|
21
|
+
print_transitively_affected_components
|
|
30
22
|
end
|
|
31
23
|
|
|
32
24
|
private
|
|
@@ -35,55 +27,23 @@ module CobraCommander
|
|
|
35
27
|
@affected ||= Affected.new(@umbrella, @changes)
|
|
36
28
|
end
|
|
37
29
|
|
|
38
|
-
def
|
|
39
|
-
changes_since_last_commit
|
|
40
|
-
directly_affected_components
|
|
41
|
-
transitively_affected_components
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def assert_valid_result_choice
|
|
45
|
-
return if %w[test full name json].include?(@format)
|
|
46
|
-
|
|
47
|
-
raise InvalidSelectionError, "--results must be 'test', 'full', 'name' or 'json'"
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def selected_format?(result)
|
|
51
|
-
@format == result
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def changes_since_last_commit
|
|
30
|
+
def print_changes_since_last_commit
|
|
55
31
|
puts "<<< Changes since last commit on #{@branch} >>>"
|
|
56
32
|
puts(*@changes) if @changes.any?
|
|
57
|
-
puts blank_line
|
|
58
33
|
end
|
|
59
34
|
|
|
60
|
-
def
|
|
35
|
+
def print_directly_affected_components
|
|
61
36
|
puts "<<< Directly affected components >>>"
|
|
62
|
-
affected.directly.each { |component|
|
|
63
|
-
puts blank_line
|
|
37
|
+
affected.directly.each { |component| display(component) }
|
|
64
38
|
end
|
|
65
39
|
|
|
66
|
-
def
|
|
40
|
+
def print_transitively_affected_components
|
|
67
41
|
puts "<<< Transitively affected components >>>"
|
|
68
|
-
affected.transitively.each { |component|
|
|
69
|
-
puts blank_line
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def tests_to_run
|
|
73
|
-
puts "<<< Test scripts to run >>>" if selected_format?("full")
|
|
74
|
-
if selected_format?("name")
|
|
75
|
-
affected.names.each { |component_name| puts component_name }
|
|
76
|
-
else
|
|
77
|
-
affected.scripts.each { |component_script| puts component_script }
|
|
78
|
-
end
|
|
42
|
+
affected.transitively.each { |component| display(component) }
|
|
79
43
|
end
|
|
80
44
|
|
|
81
45
|
def display(component)
|
|
82
|
-
"#{component.name} - #{component.packages.map(&:key).map(&:to_s).map(&:capitalize).join(' & ')}"
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def blank_line
|
|
86
|
-
""
|
|
46
|
+
puts "#{component.name} - #{component.packages.map(&:key).map(&:to_s).map(&:capitalize).join(' & ')}"
|
|
87
47
|
end
|
|
88
48
|
end
|
|
89
49
|
end
|
data/lib/cobra_commander/cli.rb
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "etc"
|
|
3
4
|
require "thor"
|
|
4
5
|
require "fileutils"
|
|
5
|
-
require "concurrent-ruby"
|
|
6
|
-
|
|
7
6
|
require "cobra_commander"
|
|
8
7
|
|
|
9
8
|
module CobraCommander
|
|
10
9
|
# Implements the tool's CLI
|
|
11
10
|
class CLI < Thor
|
|
11
|
+
DEFAULT_CONCURRENCY = (Etc.nprocessors / 2.0).ceil
|
|
12
|
+
|
|
12
13
|
require_relative "cli/filters"
|
|
13
14
|
require_relative "cli/output/ascii_tree"
|
|
14
15
|
require_relative "cli/output/change"
|
|
15
16
|
require_relative "cli/output/dot_graph"
|
|
16
17
|
|
|
17
|
-
DEFAULT_CONCURRENCY = (Concurrent.processor_count / 2.0).ceil
|
|
18
|
-
|
|
19
18
|
class_option :app, default: Dir.pwd, aliases: "-a", type: :string
|
|
20
19
|
Source.all.keys.each do |key|
|
|
21
20
|
class_option key, default: false, type: :boolean, desc: "Consider only the #{key} dependency graph"
|
|
@@ -39,46 +38,42 @@ module CobraCommander
|
|
|
39
38
|
"Defaults to all components."
|
|
40
39
|
filter_options dependents: "Run the script on each dependent of a given component",
|
|
41
40
|
dependencies: "Run the script on each dependency of a given component"
|
|
42
|
-
method_option :concurrency, type: :numeric,
|
|
43
|
-
|
|
41
|
+
method_option :concurrency, type: :numeric, aliases: "-c", desc: "Max number of jobs to run concurrently",
|
|
42
|
+
default: DEFAULT_CONCURRENCY
|
|
44
43
|
method_option :interactive, type: :boolean, default: true, aliases: "-i",
|
|
45
44
|
desc: "Runs in interactive mode to allow the user to inspect the output of each " \
|
|
46
45
|
"component"
|
|
47
46
|
def exec(script_or_components, script = nil)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
CobraCommander::Executor.execute_and_handle_exit(
|
|
48
|
+
runner: ::CobraCommander::Executor::Script.new(script || script_or_components),
|
|
49
|
+
workers: options.concurrency,
|
|
50
|
+
interactive: options.interactive,
|
|
51
|
+
jobs: components_filtered(script && script_or_components)
|
|
51
52
|
)
|
|
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?
|
|
56
53
|
end
|
|
57
54
|
|
|
58
55
|
desc "cmd [components] <command>", "Executes the command in the context of a given component or set thereof. " \
|
|
59
56
|
"Defaults to all components."
|
|
60
57
|
filter_options dependents: "Run the command on each dependent of a given component",
|
|
61
58
|
dependencies: "Run the command on each dependency of a given component"
|
|
62
|
-
method_option :concurrency, type: :numeric,
|
|
63
|
-
|
|
59
|
+
method_option :concurrency, type: :numeric, aliases: "-c", desc: "Max number of jobs to run concurrently",
|
|
60
|
+
default: DEFAULT_CONCURRENCY
|
|
64
61
|
method_option :interactive, type: :boolean, default: true, aliases: "-i",
|
|
65
62
|
desc: "Runs in interactive mode to allow the user to inspect the output of each " \
|
|
66
63
|
"component"
|
|
67
64
|
def cmd(command_or_components, command = nil)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
CobraCommander::Executor.execute_and_handle_exit(
|
|
66
|
+
runner: ::CobraCommander::Executor::Command.new(command || command_or_components),
|
|
67
|
+
workers: options.concurrency,
|
|
68
|
+
interactive: options.interactive,
|
|
69
|
+
jobs: components_filtered(command && command_or_components).flat_map(&:packages)
|
|
71
70
|
)
|
|
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?
|
|
76
71
|
end
|
|
77
72
|
|
|
78
73
|
desc "tree [component]", "Prints the dependency tree of a given component or umbrella"
|
|
79
74
|
def tree(component = nil)
|
|
80
75
|
components = component ? [find_component(component)] : umbrella.components
|
|
81
|
-
puts Output::AsciiTree.new(components)
|
|
76
|
+
puts Output::AsciiTree.new(components)
|
|
82
77
|
end
|
|
83
78
|
|
|
84
79
|
desc "graph [component]", "Outputs a graph of a given component or umbrella"
|
|
@@ -97,22 +92,18 @@ module CobraCommander
|
|
|
97
92
|
end
|
|
98
93
|
|
|
99
94
|
desc "changes [--results=RESULTS] [--branch=BRANCH]", "Prints list of changed files"
|
|
100
|
-
method_option :results, default: "test", aliases: "-r", desc: "Accepts test, full, name or json"
|
|
101
95
|
method_option :branch, default: "master", aliases: "-b", desc: "Specified target to calculate against"
|
|
102
96
|
def changes
|
|
103
|
-
Output::Change.new(umbrella, options.
|
|
97
|
+
Output::Change.new(umbrella, options.branch).run!
|
|
104
98
|
end
|
|
105
99
|
|
|
106
100
|
private
|
|
107
101
|
|
|
108
102
|
def umbrella
|
|
109
|
-
selector = Source.all.keys.reduce({}) do |sel, key|
|
|
103
|
+
selector = CobraCommander::Source.all.keys.reduce({}) do |sel, key|
|
|
110
104
|
sel.merge(key => options.public_send(key))
|
|
111
105
|
end
|
|
112
|
-
@umbrella ||= CobraCommander::Umbrella.new(
|
|
113
|
-
options.app,
|
|
114
|
-
**selector
|
|
115
|
-
)
|
|
106
|
+
@umbrella ||= CobraCommander::Umbrella.new(options.app, **selector)
|
|
116
107
|
rescue ::CobraCommander::Source::Error => e
|
|
117
108
|
error(e.message)
|
|
118
109
|
exit(1)
|
|
@@ -12,6 +12,10 @@ module CobraCommander
|
|
|
12
12
|
@packages = []
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
def describe
|
|
16
|
+
"#{name} (#{packages.map(&:key).join(', ')})"
|
|
17
|
+
end
|
|
18
|
+
|
|
15
19
|
def add_package(package)
|
|
16
20
|
@packages << package
|
|
17
21
|
@dependency_names |= package.dependencies
|
|
@@ -21,6 +25,18 @@ module CobraCommander
|
|
|
21
25
|
@packages.map(&:path).uniq
|
|
22
26
|
end
|
|
23
27
|
|
|
28
|
+
# Wraps the execution of a command on this component by nesting the
|
|
29
|
+
# around_command of each distinct source backing its packages, so every
|
|
30
|
+
# package gets a chance to enhance the surrounding process. The env vars
|
|
31
|
+
# each source yields are merged (later sources win) and the merged hash is
|
|
32
|
+
# yielded to the block.
|
|
33
|
+
#
|
|
34
|
+
# @yieldparam env [Hash{String => String}] merged env vars for the command
|
|
35
|
+
# @return the value returned by the block
|
|
36
|
+
def around_command(&)
|
|
37
|
+
nest_sources(@packages.map(&:source).uniq, {}, &)
|
|
38
|
+
end
|
|
39
|
+
|
|
24
40
|
def inspect
|
|
25
41
|
"#<CobraCommander::Component:#{object_id} #{name} dependencies=#{dependencies.map(&:name)} packages=#{packages}>"
|
|
26
42
|
end
|
|
@@ -46,5 +62,18 @@ module CobraCommander
|
|
|
46
62
|
def dependencies
|
|
47
63
|
@dependencies ||= @dependency_names.sort.filter_map { |name| @umbrella.find(name) }
|
|
48
64
|
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# Nests each source's #around_command, accumulating the env each yields,
|
|
69
|
+
# and finally yields the merged env to the block.
|
|
70
|
+
def nest_sources(sources, env, &block)
|
|
71
|
+
return yield(env) if sources.empty?
|
|
72
|
+
|
|
73
|
+
head, *rest = sources
|
|
74
|
+
head.around_command do |source_env|
|
|
75
|
+
nest_sources(rest, env.merge(source_env), &block)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
49
78
|
end
|
|
50
79
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CobraCommander
|
|
4
|
+
module Executor
|
|
5
|
+
class BufferedPrinter < TTY::Command::Printers::Null
|
|
6
|
+
TIME_FORMAT = "Finished in %5.3fs"
|
|
7
|
+
|
|
8
|
+
def initialize(*)
|
|
9
|
+
super
|
|
10
|
+
|
|
11
|
+
@buffers = Hash.new { |h, k| h[k] = StringIO.new }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def write(cmd, message)
|
|
15
|
+
@buffers[cmd.uuid].write message
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def print_command_out_data(cmd, *args)
|
|
19
|
+
write(cmd, args.join)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def print_command_err_data(cmd, *args)
|
|
23
|
+
write(cmd, args.join)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def print_command_start(cmd, *args)
|
|
27
|
+
message = "Running #{decorate(cmd.to_command, :yellow, :bold)} #{args.join(' ')}"
|
|
28
|
+
write(cmd, message)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def print_command_exit(cmd, status, runtime, *)
|
|
32
|
+
message = TIME_FORMAT % runtime
|
|
33
|
+
message << " with exit status #{status}" if status
|
|
34
|
+
|
|
35
|
+
output.puts @buffers.delete(cmd.uuid).string
|
|
36
|
+
output.puts decorate(message, status == 0 ? :green : :red)
|
|
37
|
+
output.puts
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -1,74 +1,51 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "./job"
|
|
4
|
-
require_relative "./package_criteria"
|
|
5
|
-
|
|
6
3
|
module CobraCommander
|
|
7
4
|
module Executor
|
|
8
5
|
class Command
|
|
9
|
-
include ::CobraCommander::Executor::Job
|
|
10
6
|
include ::CobraCommander::Executor::PackageCriteria
|
|
7
|
+
include ::CobraCommander::Executor::RunScript
|
|
11
8
|
|
|
12
9
|
SKIP_UNEXISTING = "Command %s does not exist. Check your cobra.yml for existing commands in %s."
|
|
13
10
|
SKIP_CRITERIA = "Package %s does not match criteria."
|
|
14
11
|
|
|
15
|
-
|
|
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
|
|
12
|
+
def initialize(command_name)
|
|
13
|
+
@command_name = command_name
|
|
24
14
|
end
|
|
25
15
|
|
|
26
16
|
# Calls the commands sequentially, stopping ony if an :error happens.
|
|
27
17
|
#
|
|
28
18
|
# If one of the commands skips, the result will be :success.
|
|
29
19
|
#
|
|
30
|
-
# @param
|
|
31
|
-
# @
|
|
32
|
-
# @
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return [:error, new_output] if result == :error
|
|
37
|
-
|
|
38
|
-
[:success, new_output]
|
|
20
|
+
# @param tty [CobraComander::Executor::IsolatedPTY] tty to execute shell scripts
|
|
21
|
+
# @param package [CobraComander::Package] target package to execute the named command
|
|
22
|
+
# @return [Array<Symbol, String>]
|
|
23
|
+
def call(tty, package)
|
|
24
|
+
package.around_command do |env|
|
|
25
|
+
run_command tty, package, @command_name, env
|
|
39
26
|
end
|
|
40
27
|
end
|
|
41
28
|
|
|
42
|
-
|
|
43
|
-
@package = package
|
|
44
|
-
@command = command
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def to_s
|
|
48
|
-
"#{@package.name} (#{@package.key})"
|
|
49
|
-
end
|
|
29
|
+
private
|
|
50
30
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
when
|
|
56
|
-
when
|
|
57
|
-
|
|
58
|
-
else run_script(command, @package.path)
|
|
31
|
+
def run_command(tty, package, command_name, env)
|
|
32
|
+
definition = package.source.config&.dig("commands", command_name)
|
|
33
|
+
case definition
|
|
34
|
+
when Array then run_multiple(tty, package, definition, env)
|
|
35
|
+
when Hash then run_with_criteria(tty, package, definition, env)
|
|
36
|
+
when nil then [:skip, format(SKIP_UNEXISTING, command_name, package.key)]
|
|
37
|
+
else run_script(tty, definition, package.path, env: env)
|
|
59
38
|
end
|
|
60
39
|
end
|
|
61
40
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def run_with_criteria(command)
|
|
65
|
-
return skip(format(SKIP_CRITERIA, @package.name)) unless match_criteria?(@package, command.fetch("if", {}))
|
|
41
|
+
def run_with_criteria(tty, package, command, env)
|
|
42
|
+
return [:skip, format(SKIP_CRITERIA, package.name)] unless match_criteria?(package, command.fetch("if", {}))
|
|
66
43
|
|
|
67
|
-
run_script(command["run"],
|
|
44
|
+
run_script(tty, command["run"], package.path, env: env)
|
|
68
45
|
end
|
|
69
46
|
|
|
70
|
-
def run_multiple(package, commands)
|
|
71
|
-
|
|
47
|
+
def run_multiple(tty, package, commands, env)
|
|
48
|
+
run_many(commands) { run_command(tty, package, _1, env) }
|
|
72
49
|
end
|
|
73
50
|
end
|
|
74
51
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CobraCommander
|
|
4
|
+
module Executor
|
|
5
|
+
#
|
|
6
|
+
# A PTY-enabled TTY::Command used to execute component scripts.
|
|
7
|
+
#
|
|
8
|
+
# Per-execution environment variables are supplied to TTY::Command#run!
|
|
9
|
+
# (so they are scoped to each subprocess and never mutate the parent
|
|
10
|
+
# ENV), while environment isolation/enhancement such as Bundler's
|
|
11
|
+
# `with_unbundled_env` is contributed by the package sources through
|
|
12
|
+
# their #around_command (see CobraCommander::Ruby::Bundle).
|
|
13
|
+
#
|
|
14
|
+
class IsolatedPTY < ::TTY::Command
|
|
15
|
+
def initialize(**)
|
|
16
|
+
super(pty: true, **)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CobraCommander
|
|
4
|
+
module Executor
|
|
5
|
+
# Runs an interactive output printer
|
|
6
|
+
class OutputPrompt
|
|
7
|
+
def self.run(pool, output = $stdout)
|
|
8
|
+
new(pool.jobs).run(output)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
pastel = Pastel.new
|
|
12
|
+
ICONS = {
|
|
13
|
+
nil => pastel.dim("⦻"),
|
|
14
|
+
success: pastel.green("✔"),
|
|
15
|
+
skip: pastel.yellow("↷"),
|
|
16
|
+
error: pastel.red("✖"),
|
|
17
|
+
}.freeze
|
|
18
|
+
CANCELLED = pastel.dim("(cancelled)")
|
|
19
|
+
BYE = pastel.bold("\n\n👋 Bye!")
|
|
20
|
+
|
|
21
|
+
def initialize(jobs)
|
|
22
|
+
@jobs = jobs
|
|
23
|
+
@prompt = TTY::Prompt.new(symbols: { cross: " " })
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def options
|
|
27
|
+
@options ||= @jobs.map do |job|
|
|
28
|
+
{
|
|
29
|
+
name: format_name(job),
|
|
30
|
+
value: job,
|
|
31
|
+
disabled: !job.resolved? && CANCELLED,
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def run(output)
|
|
37
|
+
return unless @jobs.any?(&:resolved?)
|
|
38
|
+
|
|
39
|
+
selected = nil
|
|
40
|
+
output.puts
|
|
41
|
+
loop do
|
|
42
|
+
selected = @prompt.select("Print output?", options, default: format_name(selected))
|
|
43
|
+
output.puts nil, selected.output, nil
|
|
44
|
+
rescue TTY::Reader::InputInterrupt
|
|
45
|
+
output.puts BYE
|
|
46
|
+
break
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def format_name(job)
|
|
53
|
+
return unless job
|
|
54
|
+
|
|
55
|
+
"#{ICONS[job.status]} #{job.name}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|