Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cobra commands #96

Merged
merged 12 commits into from
Nov 29, 2022
6 changes: 3 additions & 3 deletions cobra_commander-ruby/lib/cobra_commander/ruby/bundle.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ module CobraCommander
module Ruby
# Calculates ruby bundler dependencies
class Bundle < CobraCommander::Source[:ruby]
def each
specs.each do |spec|
yield ::CobraCommander::Package.new(
def packages
specs.map do |spec|
::CobraCommander::Package.new(
self,
name: spec.name,
path: Pathname.new(spec.loaded_from).dirname,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

RSpec.describe CobraCommander::Ruby::Bundle do
subject { CobraCommander::Ruby::Bundle.new(dummy_path) }
subject { CobraCommander::Ruby::Bundle.new(dummy_path, {}) }
let(:hr_package) do
subject.find do |package|
package.name.eql?("hr")
Expand Down
6 changes: 3 additions & 3 deletions cobra_commander-yarn/lib/cobra_commander/yarn/workspace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ module CobraCommander
module Yarn
# Yarn workspace components source for an umbrella
class Workspace < CobraCommander::Source[:js]
def each
workspace_data.each do |name, spec|
yield ::CobraCommander::Package.new(
def packages
workspace_data.map do |name, spec|
::CobraCommander::Package.new(
self,
path: path.join(spec["location"]),
dependencies: spec["workspaceDependencies"].map { |d| untag(d) },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

RSpec.describe CobraCommander::Yarn::Workspace do
subject { CobraCommander::Yarn::Workspace.new(dummy_path) }
subject { CobraCommander::Yarn::Workspace.new(dummy_path, {}) }
let(:hr_package) do
subject.find do |package|
package.name.eql?("hr-ui")
Expand Down Expand Up @@ -30,7 +30,7 @@
end

it "throws a CobraCommander::Source::Error when not a valid workspace" do
workspace = CobraCommander::Yarn::Workspace.new(__dir__)
workspace = CobraCommander::Yarn::Workspace.new(__dir__, {})

expect { workspace.to_a }.to raise_error CobraCommander::Source::Error
end
Expand Down
36 changes: 25 additions & 11 deletions cobra_commander/lib/cobra_commander/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ class CLI < Thor
require_relative "cli/output/ascii_tree"
require_relative "cli/output/change"
require_relative "cli/output/dot_graph"
require_relative "cli/output/interactive_printer"
require_relative "cli/output/markdown_printer"

DEFAULT_CONCURRENCY = (Concurrent.processor_count / 2.0).ceil

Expand Down Expand Up @@ -47,16 +45,32 @@ def ls(components = nil)
desc: "Runs in interactive mode to allow the user to inspect the output of each " \
"component"
def exec(script_or_components, script = nil)
result = CobraCommander::Executor.execute_script(
components: components_filtered(script && script_or_components),
script: script || script_or_components,
workers: options.concurrency, status_output: $stderr
jobs = CobraCommander::Executor::Script.for(
components_filtered(script && script_or_components),
script || script_or_components
)
if options.interactive && result.count > 1
Output::InteractivePrinter.run(result, $stdout)
else
Output::MarkdownPrinter.run(result, $stdout)
end
CobraCommander::Executor.execute(jobs: jobs, workers: options.concurrency,
output_mode: options.interactive && jobs.count > 1 ? :interactive : :markdown,
output: $stdout, status_output: $stderr)
end

desc "cmd [components] <command>", "Executes the command in the context of a given component or set thereof. " \
"Defaults to all components."
filter_options dependents: "Run the command on each dependent of a given component",
dependencies: "Run the command on each dependency of a given component"
method_option :concurrency, type: :numeric, default: DEFAULT_CONCURRENCY, aliases: "-c",
desc: "Max number of jobs to run concurrently"
method_option :interactive, type: :boolean, default: true, aliases: "-i",
desc: "Runs in interactive mode to allow the user to inspect the output of each " \
"component"
def cmd(command_or_components, command = nil)
jobs = CobraCommander::Executor::Command.for(
components_filtered(command && command_or_components),
command || command_or_components
)
CobraCommander::Executor.execute(jobs: jobs, workers: options.concurrency,
output_mode: options.interactive && jobs.count > 1 ? :interactive : :markdown,
output: $stdout, status_output: $stderr)
end

desc "tree [component]", "Prints the dependency tree of a given component or umbrella"
Expand Down
43 changes: 23 additions & 20 deletions cobra_commander/lib/cobra_commander/executor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,43 @@
require_relative "executor/execution"
require_relative "executor/job"
require_relative "executor/script"
require_relative "executor/command"
require_relative "executor/spinners"
require_relative "executor/interactive_printer"
require_relative "executor/markdown_printer"

module CobraCommander
# Execute a command on all given packages
module Executor
module_function

# Executes the given script in all Components.
# Executes the given jobs in an CobraCommander::Executor::Execution.
#
# If a component has two packages in the same path, the script will run only once.
# This facade also allows to execute the jobs with a spinner (@see CobraCommander::Executor::Spinners) to display
# the execution status of each job.
#
# @param components [Enumerable<CobraCommander::Component>] the target components
# @param script [String] shell script to run from the directories of the component's packages
# @param workers [Integer] number of workers processing the job queue
# @return [CobraCommander::Executor::Execution]
# @see .execute
def execute_script(components:, script:, **kwargs)
packages = components.flat_map(&:packages).uniq(&:path)
jobs = packages.map { |package| Script.new(package.name, package.path, script) }

execute jobs: jobs, **kwargs
end

# Executes the given jobs, in an Execution
# You can also determine how to display the execution once it's completed, by setting `output_mode` to either
# :interactive or :markdown. When using :interactive, a menu with each job will be displayed allowing the user
# to select a job and see its output. When using :markdown, a markdown will be printed to `output` with the
# output of each job.
#
# @param jobs [Enumerable<CobraCommander::Executor::Job>] the jobs to run
# @param status_output [IO,nil] if not nil, will print the spinners for each job in this output
# @param workers [Integer] number of workers processing the jobs queue
# @param workers [Integer] number of workers processing the jobs queue (see CobraCommander::Executor::Execution)
# @param output_mode [:interactive,:markdown,nil] how the output will be printed after execution
# @param workers [Integer] number of workers processing the jobs queue (see CobraCommander::Executor::Execution)
# @return [CobraCommander::Executor::Execution]
# @see CobraCommander::Executor::Execution
def execute(jobs:, status_output: nil, **kwargs)
execution = Execution.new(jobs, **kwargs)
Spinners.start(execution, output: status_output) if status_output
execution.tap(&:wait)
# @see CobraCommander::Executor::Spinners
# @see CobraCommander::Executor::InterativePrinter
# @see CobraCommander::Executor::MarkdownPrinter
def execute(jobs:, status_output: nil, output_mode: nil, output: nil, **kwargs)
Execution.new(jobs, **kwargs).tap do |execution|
Spinners.start(execution, output: status_output) if status_output
execution.wait
InteractivePrinter.run(execution, output) if output_mode == :interactive
MarkdownPrinter.run(execution, output) if output_mode == :markdown
end
end
end
end
44 changes: 44 additions & 0 deletions cobra_commander/lib/cobra_commander/executor/command.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

require_relative "./job"

module CobraCommander
module Executor
class Command
include ::CobraCommander::Executor::Job

SKIP_UNEXISTING = "Command %s does not exist. Check your cobra.yml for existing commands in %s."

# Executes the given script in all Components.
#
# If a component has two packages in the same path, the script will run only once.
#
# @param components [Enumerable<CobraCommander::Component>] the target components
# @param script [String] shell script to run from the directories of the component's packages
# @param workers [Integer] number of workers processing the job queue
# @return [CobraCommander::Executor::Execution]
# @see .execute
def self.for(components, command)
components.flat_map(&:packages).map do |package|
new(package, command)
end
end

def initialize(package, command)
@package = package
@command = command
end

def to_s
"#{@package.name} (#{@package.key})"
end

def call
script = @package.source.config&.dig("commands", @command)
return skip(format(SKIP_UNEXISTING, @command, @package.key)) unless script

run_script script, @package.path
end
end
end
end
9 changes: 4 additions & 5 deletions cobra_commander/lib/cobra_commander/executor/execution.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,10 @@ def wait

def create_future(job)
Concurrent::Promises.future_on(@executor, job, &:call).then do |result|
case result
in [:error, error]
Concurrent::Promises.rejected_future(error)
in [:success, output]
Concurrent::Promises.fulfilled_future(output)
status, output = result
case status
when :error then Concurrent::Promises.rejected_future(output)
when :success, :skip then Concurrent::Promises.fulfilled_future(output)
else
Concurrent::Promises.fulfilled_future(result)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
require "tty-prompt"

module CobraCommander
module CLI::Output
module Executor
# Runs an interactive output printer
class InteractivePrinter
pastel = Pastel.new
Expand Down
26 changes: 26 additions & 0 deletions cobra_commander/lib/cobra_commander/executor/job.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "tty-command"

module CobraCommander
module Executor
#
Expand All @@ -13,13 +15,37 @@ module Executor
# (i.e.: [:error, "string output"] or just "string output").
#
module Job
def skip(reason)
[:skip, reason]
end

def error(output)
[:error, output]
end

def success(output)
[:success, output]
end

def run_script(script, path)
result = isolate_bundle do
TTY::Command.new(pty: true, printer: :null)
.run!(script, chdir: path, err: :out)
end
return error(result.out) if result.failed?

success(result.out)
end

private

def isolate_bundle(&block)
if Bundler.respond_to?(:with_unbundled_env)
Bundler.with_unbundled_env(&block)
else
Bundler.with_clean_env(&block)
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# frozen_string_literal: true

require "tty-prompt"

module CobraCommander
module CLI::Output
module Executor
# Prints the given CobraCommander::Executor::Context to [output] collection in markdown
module MarkdownPrinter
SUCCESS = "\n## ✔ %s\n"
Expand All @@ -14,7 +12,7 @@ def self.run(execution, output)
execution.each do |job, result|
template = result.fulfilled? ? SUCCESS : ERROR

output.print format(template, job.name)
output.print format(template, job)
output.print format(OUTPUT, result.fulfilled? ? result.value : result.reason)
end
end
Expand Down
49 changes: 24 additions & 25 deletions cobra_commander/lib/cobra_commander/executor/script.rb
Original file line number Diff line number Diff line change
@@ -1,44 +1,43 @@
# frozen_string_literal: true

require "tty-command"

require_relative "./job"

module CobraCommander
module Executor
# This is a script job. It can tarket any CobraCommander::Package.
#
# If you want to target a Component, you can use Script.for to target
# individual paths for each given component.
#
# @see Script.for
class Script
include ::CobraCommander::Executor::Job

attr_reader :name, :command, :result
# Returns a set of scripts to be executed on the given commends.
#
# If a component has two packages in the same path, only one script for that component will be
# returned.
#
# @param components [Enumerable<CobraCommander::Component>] the target components
# @param script [String] shell script to run from the directories of the component's packages
# @return [Array<CobraCommander::Executor::Script>]
def self.for(components, script)
components.flat_map(&:packages).uniq(&:path).map do |package|
new(package, script)
end
end

def initialize(name, path, command)
@name = name
@command = command
@path = path
@tty = TTY::Command.new(pty: true, printer: :null)
def initialize(package, script)
@package = package
@script = script
end

def to_s
@name
@package.name
end

def call
isolate_bundle do
result = @tty.run!(@command, chdir: @path)
return error(result.err) if result.failed?

success(result.out)
end
end

private

def isolate_bundle(&block)
if Bundler.respond_to?(:with_unbundled_env)
Bundler.with_unbundled_env(&block)
else
Bundler.with_clean_env(&block)
end
run_script @script, @package.path
end
end
end
Expand Down
Loading