capistrano_multiconfig_parallel 0.0.1
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 +7 -0
- data/.coveralls.yml +2 -0
- data/.gitignore +21 -0
- data/.rspec +1 -0
- data/.rubocop.yml +68 -0
- data/.travis.yml +12 -0
- data/CONTRIBUTING.md +44 -0
- data/Gemfile +3 -0
- data/Guardfile +12 -0
- data/LICENSE +20 -0
- data/README.md +220 -0
- data/Rakefile +56 -0
- data/bin/multi_cap +7 -0
- data/capistrano_multiconfig_parallel.gemspec +51 -0
- data/img/parallel_demo.png +0 -0
- data/init.rb +1 -0
- data/lib/capistrano_multiconfig_parallel/application.rb +57 -0
- data/lib/capistrano_multiconfig_parallel/base.rb +92 -0
- data/lib/capistrano_multiconfig_parallel/celluloid/celluloid_manager.rb +178 -0
- data/lib/capistrano_multiconfig_parallel/celluloid/celluloid_worker.rb +238 -0
- data/lib/capistrano_multiconfig_parallel/celluloid/child_process.rb +104 -0
- data/lib/capistrano_multiconfig_parallel/celluloid/rake_worker.rb +83 -0
- data/lib/capistrano_multiconfig_parallel/celluloid/state_machine.rb +49 -0
- data/lib/capistrano_multiconfig_parallel/celluloid/terminal_table.rb +122 -0
- data/lib/capistrano_multiconfig_parallel/cli.rb +55 -0
- data/lib/capistrano_multiconfig_parallel/configuration.rb +70 -0
- data/lib/capistrano_multiconfig_parallel/helpers/base_manager.rb +217 -0
- data/lib/capistrano_multiconfig_parallel/helpers/multi_app_manager.rb +84 -0
- data/lib/capistrano_multiconfig_parallel/helpers/single_app_manager.rb +48 -0
- data/lib/capistrano_multiconfig_parallel/helpers/standard_deploy.rb +40 -0
- data/lib/capistrano_multiconfig_parallel/initializers/conf.rb +6 -0
- data/lib/capistrano_multiconfig_parallel/initializers/confirm_question.rb +25 -0
- data/lib/capistrano_multiconfig_parallel/initializers/i18n.rb +10 -0
- data/lib/capistrano_multiconfig_parallel/initializers/rake.rb +28 -0
- data/lib/capistrano_multiconfig_parallel/multi_app_helpers/dependency_tracker.rb +111 -0
- data/lib/capistrano_multiconfig_parallel/multi_app_helpers/interactive_menu.rb +61 -0
- data/lib/capistrano_multiconfig_parallel/version.rb +16 -0
- data/lib/capistrano_multiconfig_parallel.rb +2 -0
- data/spec/spec_helper.rb +48 -0
- metadata +648 -0
| @@ -0,0 +1,49 @@ | |
| 1 | 
            +
            module CapistranoMulticonfigParallel
         | 
| 2 | 
            +
              # class that handles the states of the celluloid worker executing the child process in a fork process
         | 
| 3 | 
            +
              class StateMachine
         | 
| 4 | 
            +
                include ComposableStateMachine::CallbackRunner
         | 
| 5 | 
            +
                attr_accessor :state, :model, :machine, :job, :initial_state, :transitions, :output
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def initialize(job, actor)
         | 
| 8 | 
            +
                  @job = job
         | 
| 9 | 
            +
                  @actor = actor
         | 
| 10 | 
            +
                  @initial_state = :unstarted
         | 
| 11 | 
            +
                  @model = generate_model
         | 
| 12 | 
            +
                  build_machine
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def go_to_transition(action)
         | 
| 16 | 
            +
                  @machine.trigger(action.to_s)
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
              private
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def build_machine
         | 
| 22 | 
            +
                  @machine = ComposableStateMachine::MachineWithExternalState.new(
         | 
| 23 | 
            +
                    @model, method(:state), method(:state=), state: initial_state.to_s, callback_runner: self)
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def generate_transitions
         | 
| 27 | 
            +
                  @transitions = ComposableStateMachine::Transitions.new
         | 
| 28 | 
            +
                  @transitions
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def generate_model
         | 
| 32 | 
            +
                  ComposableStateMachine.model(
         | 
| 33 | 
            +
                    transitions: generate_transitions,
         | 
| 34 | 
            +
                    behaviors: {
         | 
| 35 | 
            +
                      enter: {
         | 
| 36 | 
            +
                        any: proc do |current_state, event, new_state|
         | 
| 37 | 
            +
                          actor_notify_state_change(current_state, event, new_state)
         | 
| 38 | 
            +
                        end
         | 
| 39 | 
            +
                      }
         | 
| 40 | 
            +
                    },
         | 
| 41 | 
            +
                    initial_state: @initial_state
         | 
| 42 | 
            +
                  )
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def actor_notify_state_change(current_state, event, new_state)
         | 
| 46 | 
            +
                  @actor.send_msg(CapistranoMulticonfigParallel::TerminalTable::TOPIC, type: 'event', message: "Going from #{current_state} to #{new_state}  due to a #{event} event")
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
              end
         | 
| 49 | 
            +
            end
         | 
| @@ -0,0 +1,122 @@ | |
| 1 | 
            +
             | 
| 2 | 
            +
            module CapistranoMulticonfigParallel
         | 
| 3 | 
            +
              # class used to display the progress of each worker on terminal screen using a table
         | 
| 4 | 
            +
              # rubocop:disable ClassLength
         | 
| 5 | 
            +
              class TerminalTable
         | 
| 6 | 
            +
                include Celluloid
         | 
| 7 | 
            +
                include Celluloid::Notifications
         | 
| 8 | 
            +
                include Celluloid::Logger
         | 
| 9 | 
            +
                TOPIC = 'sshkit_terminal'
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def initialize(manager)
         | 
| 12 | 
            +
                  @manager = manager
         | 
| 13 | 
            +
                  async.run
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def run
         | 
| 17 | 
            +
                  subscribe(CapistranoMulticonfigParallel::TerminalTable::TOPIC, :notify_time_change)
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def notify_time_change(topic, message)
         | 
| 21 | 
            +
                  return unless topic == CapistranoMulticonfigParallel::TerminalTable::TOPIC
         | 
| 22 | 
            +
                  default_headings = ['Job ID', 'App/Stage', 'Action', 'ENV Variables', 'Current Task']
         | 
| 23 | 
            +
                  if CapistranoMulticonfigParallel.show_task_progress
         | 
| 24 | 
            +
                    default_headings << 'Total'
         | 
| 25 | 
            +
                    default_headings << 'Progress'
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
                  table = Terminal::Table.new(title: 'Deployment Status Table', headings: default_headings)
         | 
| 28 | 
            +
                  if @manager.jobs.present? && message_valid?(message)
         | 
| 29 | 
            +
                    @manager.jobs.each do |job_id, _job|
         | 
| 30 | 
            +
                      add_job_to_table(table, job_id)
         | 
| 31 | 
            +
                    end
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
                  show_terminal_screen(table)
         | 
| 34 | 
            +
                rescue => ex
         | 
| 35 | 
            +
                  info "Terminal Table  client disconnected due to error #{ex.inspect}"
         | 
| 36 | 
            +
                  info ex.backtrace
         | 
| 37 | 
            +
                  terminate
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                def message_valid?(message)
         | 
| 41 | 
            +
                  message[:type].present? && message[:type] == 'output' || message[:type] == 'event'
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                def show_terminal_screen(table)
         | 
| 45 | 
            +
                  return unless table.rows.present?
         | 
| 46 | 
            +
                  terminal_clear
         | 
| 47 | 
            +
                  puts "\n"
         | 
| 48 | 
            +
                  #  table.style = { width: 20 }
         | 
| 49 | 
            +
                  puts table
         | 
| 50 | 
            +
                  puts "\n"
         | 
| 51 | 
            +
                  sleep(1)
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                def add_job_to_table(table, job_id)
         | 
| 55 | 
            +
                  worker = @manager.get_worker_for_job(job_id)
         | 
| 56 | 
            +
                  return unless worker.present?
         | 
| 57 | 
            +
                  worker_optons = ''
         | 
| 58 | 
            +
                  worker.env_options.each do |key, value|
         | 
| 59 | 
            +
                    worker_optons << "#{key}=#{value}\n"
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
                  state = worker.machine.state.to_s
         | 
| 62 | 
            +
                  state = worker_crashed?(worker) ? state.red : state.green
         | 
| 63 | 
            +
                  row = [{ value: worker.job_id.to_s },
         | 
| 64 | 
            +
                         { value: "#{worker.app_name}\n#{worker.env_name}" },
         | 
| 65 | 
            +
                         { value: worker.action_name },
         | 
| 66 | 
            +
                         { value: worker_optons },
         | 
| 67 | 
            +
                         { value: state }
         | 
| 68 | 
            +
                        ]
         | 
| 69 | 
            +
                  if CapistranoMulticonfigParallel.show_task_progress
         | 
| 70 | 
            +
                    row << { value: worker.rake_tasks.size }
         | 
| 71 | 
            +
                    row << { value: worker_progress(worker) }
         | 
| 72 | 
            +
                  end
         | 
| 73 | 
            +
                  table.add_row(row)
         | 
| 74 | 
            +
                  table.add_separator if @manager.jobs.keys.last.to_i != job_id.to_i
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                def terminal_clear
         | 
| 78 | 
            +
                  system('cls') || system('clear') || puts("\e[H\e[2J")
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                def worker_crashed?(worker)
         | 
| 82 | 
            +
                  worker.crashed?
         | 
| 83 | 
            +
                end
         | 
| 84 | 
            +
                # rubocop:disable Lint/Eval
         | 
| 85 | 
            +
                def capture(stream)
         | 
| 86 | 
            +
                  stream = stream.to_s
         | 
| 87 | 
            +
                  captured_stream = Tempfile.new(stream)
         | 
| 88 | 
            +
                  stream_io = eval("$#{stream}")
         | 
| 89 | 
            +
                  origin_stream = stream_io.dup
         | 
| 90 | 
            +
                  stream_io.reopen(captured_stream)
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                  yield
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                  stream_io.rewind
         | 
| 95 | 
            +
                  return captured_stream.read
         | 
| 96 | 
            +
                ensure
         | 
| 97 | 
            +
                  captured_stream.close
         | 
| 98 | 
            +
                  captured_stream.unlink
         | 
| 99 | 
            +
                  stream_io.reopen(origin_stream)
         | 
| 100 | 
            +
                end
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                def worker_progress(worker)
         | 
| 103 | 
            +
                  tasks = worker.rake_tasks
         | 
| 104 | 
            +
                  current_task = worker.machine.state
         | 
| 105 | 
            +
                  total_tasks = tasks.size
         | 
| 106 | 
            +
                  task_index = tasks.index(current_task)
         | 
| 107 | 
            +
                  progress = Formatador::ProgressBar.new(total_tasks, color: 'green', start: task_index.to_i)
         | 
| 108 | 
            +
                  result = capture(:stdout) do
         | 
| 109 | 
            +
                    progress.increment
         | 
| 110 | 
            +
                  end
         | 
| 111 | 
            +
                  result = result.gsub("\r\n", '')
         | 
| 112 | 
            +
                  result = result.gsub("\n", '')
         | 
| 113 | 
            +
                  result = result.gsub('|', '#')
         | 
| 114 | 
            +
                  result = result.gsub(/\s+/, ' ')
         | 
| 115 | 
            +
                  if worker_crashed?(worker)
         | 
| 116 | 
            +
                    return result.red
         | 
| 117 | 
            +
                  else
         | 
| 118 | 
            +
                    return result.green
         | 
| 119 | 
            +
                  end
         | 
| 120 | 
            +
                end
         | 
| 121 | 
            +
              end
         | 
| 122 | 
            +
            end
         | 
| @@ -0,0 +1,55 @@ | |
| 1 | 
            +
            require 'rubygems'
         | 
| 2 | 
            +
            require 'bundler'
         | 
| 3 | 
            +
            require 'bundler/setup'
         | 
| 4 | 
            +
            require 'rake'
         | 
| 5 | 
            +
            require 'active_support/core_ext/object/blank'
         | 
| 6 | 
            +
            require 'active_support/core_ext/hash/keys'
         | 
| 7 | 
            +
            require 'active_support/concern'
         | 
| 8 | 
            +
            require 'celluloid/autostart'
         | 
| 9 | 
            +
            require 'celluloid/pmap'
         | 
| 10 | 
            +
            require 'composable_state_machine'
         | 
| 11 | 
            +
            require 'formatador'
         | 
| 12 | 
            +
            require 'eventmachine'
         | 
| 13 | 
            +
            require 'right_popen'
         | 
| 14 | 
            +
            require 'colorize'
         | 
| 15 | 
            +
            require 'logger'
         | 
| 16 | 
            +
            require 'terminal-table'
         | 
| 17 | 
            +
            require 'colorize'
         | 
| 18 | 
            +
            require 'celluloid_pubsub'
         | 
| 19 | 
            +
            require 'capistrano/all'
         | 
| 20 | 
            +
            require 'fileutils'
         | 
| 21 | 
            +
            require 'logger'
         | 
| 22 | 
            +
            require 'pp'
         | 
| 23 | 
            +
            require 'configurations'
         | 
| 24 | 
            +
            # fix error with not files that can not be found
         | 
| 25 | 
            +
            Gem.find_files('composable_state_machine/**/*.rb').each { |path| require path }
         | 
| 26 | 
            +
             | 
| 27 | 
            +
            Gem.find_files('capistrano_multiconfig_parallel/initializers/**/*.rb').each { |path| require path }
         | 
| 28 | 
            +
            Gem.find_files('capistrano_multiconfig_parallel/helpers/**/*.rb').each { |path| require path }
         | 
| 29 | 
            +
            Gem.find_files('capistrano_multiconfig_parallel/celluloid/**/*.rb').each { |path| require path }
         | 
| 30 | 
            +
             | 
| 31 | 
            +
            require_relative './base'
         | 
| 32 | 
            +
            require_relative 'application'
         | 
| 33 | 
            +
             | 
| 34 | 
            +
            module CapistranoMulticonfigParallel
         | 
| 35 | 
            +
              # this is the class that will be invoked from terminal , and willl use the invoke task as the primary function.
         | 
| 36 | 
            +
              class CLI
         | 
| 37 | 
            +
                def self.start
         | 
| 38 | 
            +
                  if $stdin.isatty
         | 
| 39 | 
            +
                    $stdin.sync = true
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
                  if $stdout.isatty
         | 
| 42 | 
            +
                    $stdout.sync = true
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
                  CapistranoMulticonfigParallel.configuration_valid?
         | 
| 45 | 
            +
                  CapistranoMulticonfigParallel.verify_app_dependencies(stages) if CapistranoMulticonfigParallel.configuration.track_dependencies
         | 
| 46 | 
            +
                  CapistranoMulticonfigParallel::Application.new.run
         | 
| 47 | 
            +
                rescue Interrupt
         | 
| 48 | 
            +
                  `stty icanon echo`
         | 
| 49 | 
            +
                  $stderr.puts 'Command cancelled.'
         | 
| 50 | 
            +
                rescue => error
         | 
| 51 | 
            +
                  $stderr.puts error
         | 
| 52 | 
            +
                  exit(1)
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
              end
         | 
| 55 | 
            +
            end
         | 
| @@ -0,0 +1,70 @@ | |
| 1 | 
            +
            require_relative './initializers/conf'
         | 
| 2 | 
            +
            module CapistranoMulticonfigParallel
         | 
| 3 | 
            +
              # class that holds the options that are configurable for this gem
         | 
| 4 | 
            +
              module Configuration
         | 
| 5 | 
            +
                extend ActiveSupport::Concern
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                included do
         | 
| 8 | 
            +
                  include Configurations
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  configurable Hash, :websocket_server
         | 
| 11 | 
            +
                  configurable Array, :development_stages
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  configurable :track_dependencies do |value|
         | 
| 14 | 
            +
                    check_boolean(:track_dependencies, value)
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  configurable Array, :application_dependencies do |value|
         | 
| 18 | 
            +
                    value.reject { |val| val.blank? || !val.is_a?(Hash) }
         | 
| 19 | 
            +
                    wrong = value.find do|hash|
         | 
| 20 | 
            +
                      !Set[:app, :priority, :dependencies].subset?(hash.keys.to_set) ||
         | 
| 21 | 
            +
                      hash[:app].blank? ||
         | 
| 22 | 
            +
                      hash[:priority].blank?
         | 
| 23 | 
            +
                      !hash[:priority].is_a?(Numeric) ||
         | 
| 24 | 
            +
                      !hash[:dependencies].is_a?(Array)
         | 
| 25 | 
            +
                    end
         | 
| 26 | 
            +
                    raise ArgumentError, "invalid configuration for #{wrong.inspect}" if wrong.present?
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  configurable :task_confirmation_active do |value|
         | 
| 30 | 
            +
                    check_boolean(:task_confirmation_active, value)
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  configurable Array, :task_confirmations do |value|
         | 
| 34 | 
            +
                    value.reject(&:blank?)
         | 
| 35 | 
            +
                    if value.find { |row| !row.is_a?(String) }
         | 
| 36 | 
            +
                      raise ArgumentError, 'the array must contain only task names'
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  configuration_defaults do |c|
         | 
| 41 | 
            +
                    c.task_confirmations = ['deploy:symlink:release']
         | 
| 42 | 
            +
                    c.task_confirmation_active = false
         | 
| 43 | 
            +
                    c.track_dependencies = false
         | 
| 44 | 
            +
                    c.websocket_server = { enable_debug: false }
         | 
| 45 | 
            +
                    c.development_stages = ['development', 'webdev']
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  not_configured do |prop| # omit the arguments to get a catch-all not_configured
         | 
| 49 | 
            +
                    raise NoMethodError, "Please configure the property `#{prop}` by assigning a value of type #{configuration.property_type(prop)}"
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                  def self.value_is_boolean?(value)
         | 
| 53 | 
            +
                    [true, false, 'true', 'false'].include?(value)
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  def self.check_boolean(prop, value)
         | 
| 57 | 
            +
                    unless value_is_boolean?(value)
         | 
| 58 | 
            +
                      raise ArgumentError, "the property `#{prop}` must be boolean"
         | 
| 59 | 
            +
                    end
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                  def self.configuration_valid?
         | 
| 63 | 
            +
                    configuration.nil? &&
         | 
| 64 | 
            +
                      configuration.task_confirmations &&
         | 
| 65 | 
            +
                      ((configuration.track_dependencies && configuration.application_dependencies) ||
         | 
| 66 | 
            +
                        configuration.track_dependencies == false)
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
                end
         | 
| 69 | 
            +
              end
         | 
| 70 | 
            +
            end
         | 
| @@ -0,0 +1,217 @@ | |
| 1 | 
            +
            require_relative './standard_deploy'
         | 
| 2 | 
            +
            module CapistranoMulticonfigParallel
         | 
| 3 | 
            +
              # finds app dependencies, shows menu and delegates jobs to celluloid manager
         | 
| 4 | 
            +
              # rubocop:disable ClassLength
         | 
| 5 | 
            +
              class BaseManager
         | 
| 6 | 
            +
                include Celluloid
         | 
| 7 | 
            +
                include Celluloid::Logger
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                attr_accessor :condition, :manager, :deps, :application, :stage, :name, :args, :argv, :jobs, :job_registered_condition, :default_stage
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def initialize(cap_app, top_level_tasks, stages)
         | 
| 12 | 
            +
                  @cap_app = cap_app
         | 
| 13 | 
            +
                  @top_level_tasks = top_level_tasks
         | 
| 14 | 
            +
                  @stages = stages
         | 
| 15 | 
            +
                  @jobs = []
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def can_start?
         | 
| 19 | 
            +
                  @top_level_tasks.size > 1 && (stages.include?(@top_level_tasks.first) || custom_command?) && ENV[CapistranoMulticonfigParallel::ENV_KEY_JOB_ID].blank?
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def custom_command?
         | 
| 23 | 
            +
                  @top_level_tasks.first == 'ensure_stage' && !stages.include?(@top_level_tasks.second) && !stages.include?(@top_level_tasks.first) && custom_commands.values.include?(@top_level_tasks.second)
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def custom_commands
         | 
| 27 | 
            +
                  key = multi_apps? ? CapistranoMulticonfigParallel::MULTI_KEY : CapistranoMulticonfigParallel::SINGLE_KEY
         | 
| 28 | 
            +
                  CapistranoMulticonfigParallel::CUSTOM_COMMANDS[key]
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def multi_apps?
         | 
| 32 | 
            +
                  @cap_app.multi_apps?
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def start(&block)
         | 
| 36 | 
            +
                  @application = custom_command? ? nil : @top_level_tasks.first.split(':').reverse[1]
         | 
| 37 | 
            +
                  @stage = custom_command? ? nil : @top_level_tasks.first.split(':').reverse[0]
         | 
| 38 | 
            +
                  @name, @args = @cap_app.parse_task_string(@top_level_tasks.second)
         | 
| 39 | 
            +
                  @argv = @cap_app.handle_options.delete_if { |arg| arg == @stage || arg == @name || arg == @top_level_tasks.first }
         | 
| 40 | 
            +
                  @argv = multi_fetch_argv(@argv)
         | 
| 41 | 
            +
                  CapistranoMulticonfigParallel.enable_logging
         | 
| 42 | 
            +
                  block.call if block_given?
         | 
| 43 | 
            +
                  run
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                def collect_jobs(options = {}, &block)
         | 
| 47 | 
            +
                  options = prepare_options(options)
         | 
| 48 | 
            +
                  block.call(options) if block_given?
         | 
| 49 | 
            +
                rescue => e
         | 
| 50 | 
            +
                  raise [e, e.backtrace].inspect
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                def process_jobs(&block)
         | 
| 54 | 
            +
                  return unless @jobs.present?
         | 
| 55 | 
            +
                  if CapistranoMulticonfigParallel.execute_in_sequence
         | 
| 56 | 
            +
                    @jobs.each { |job| CapistranoMulticonfigParallel::StandardDeploy.execute_standard_deploy(job) }
         | 
| 57 | 
            +
                  else
         | 
| 58 | 
            +
                    run_async_jobs(&block)
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
                
         | 
| 62 | 
            +
                def fetch_multi_stages
         | 
| 63 | 
            +
                  stages = @argv['STAGES'].blank? ? '' : @argv['STAGES']
         | 
| 64 | 
            +
                  stages = parse_inputted_value(value: stages).split(',').compact if stages.present?
         | 
| 65 | 
            +
                  stages
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
              
         | 
| 68 | 
            +
                
         | 
| 69 | 
            +
                def deploy_app(options = {})
         | 
| 70 | 
            +
                  options = options.stringify_keys
         | 
| 71 | 
            +
                  app = options['app'].is_a?(Hash) ?  options['app'] : {'app' =>  options['app'] }
         | 
| 72 | 
            +
                  branch = @branch_backup.present? ? @branch_backup : @argv['BRANCH'].to_s
         | 
| 73 | 
            +
                  call_task_deploy_app({
         | 
| 74 | 
            +
                      branch: branch,
         | 
| 75 | 
            +
                      app: app ,
         | 
| 76 | 
            +
                      action: options['action']
         | 
| 77 | 
            +
                    }.reverse_merge(options))
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                private
         | 
| 81 | 
            +
              
         | 
| 82 | 
            +
                def call_task_deploy_app(options = {})
         | 
| 83 | 
            +
                  options = options.stringify_keys
         | 
| 84 | 
            +
                  main_box_name = @argv['BOX'].blank? ? '' : @argv['BOX']
         | 
| 85 | 
            +
                  stage = options.fetch('stage', @default_stage )
         | 
| 86 | 
            +
                  if CapistranoMulticonfigParallel.configuration.development_stages.include?(stage) && main_box_name.present? && /^[a-z0-9,]+/.match(main_box_name)
         | 
| 87 | 
            +
                    execute_on_multiple_boxes(main_box_name, options)
         | 
| 88 | 
            +
                  else
         | 
| 89 | 
            +
                    prepare_job(options)
         | 
| 90 | 
            +
                  end
         | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                def run_async_jobs(&block)
         | 
| 94 | 
            +
                  return unless @jobs.present?
         | 
| 95 | 
            +
                  @condition = Celluloid::Condition.new
         | 
| 96 | 
            +
                  @manager = CapistranoMulticonfigParallel::CelluloidManager.new(Actor.current)
         | 
| 97 | 
            +
                  @jobs.pmap do |job|
         | 
| 98 | 
            +
                    @manager.async.delegate(job)
         | 
| 99 | 
            +
                  end
         | 
| 100 | 
            +
                  until @manager.registration_complete
         | 
| 101 | 
            +
                    sleep(0.1) # keep current thread alive
         | 
| 102 | 
            +
                  end
         | 
| 103 | 
            +
                  return unless @manager.registration_complete
         | 
| 104 | 
            +
                  @manager.process_jobs(&block)
         | 
| 105 | 
            +
                  wait_jobs_termination
         | 
| 106 | 
            +
                end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                def wait_jobs_termination
         | 
| 109 | 
            +
                  return if @jobs.blank? || CapistranoMulticonfigParallel.execute_in_sequence
         | 
| 110 | 
            +
                  result = @condition.wait
         | 
| 111 | 
            +
                  return unless result.present?
         | 
| 112 | 
            +
                  @manager.terminate
         | 
| 113 | 
            +
                  terminate
         | 
| 114 | 
            +
                end
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                def prepare_job(options)
         | 
| 117 | 
            +
                  options = options.stringify_keys
         | 
| 118 | 
            +
                  branch_name = options.fetch('branch', {})
         | 
| 119 | 
            +
                  app = options.fetch('app', {})
         | 
| 120 | 
            +
                  app = app.fetch('app', '')
         | 
| 121 | 
            +
                  box = options['env_options']['BOX']
         | 
| 122 | 
            +
                  message = box.present? ? "BOX #{box}:" : "stage #{options['stage']}:"
         | 
| 123 | 
            +
                  branch_name = get_applications_branch_from_stdin(app, message) if branch_name.blank?
         | 
| 124 | 
            +
                  env_opts = get_app_additional_env_options(app, message)
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                  options['env_options'] = options['env_options'].reverse_merge(env_opts)
         | 
| 127 | 
            +
                  env_options = {
         | 
| 128 | 
            +
                    'BRANCH' => branch_name
         | 
| 129 | 
            +
                  }.merge(options['env_options'])
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                  job = {
         | 
| 132 | 
            +
                    app: app,
         | 
| 133 | 
            +
                    env: options['stage'],
         | 
| 134 | 
            +
                    action: options['action'],
         | 
| 135 | 
            +
                    task_arguments: options['task_arguments'],
         | 
| 136 | 
            +
                    env_options: env_options
         | 
| 137 | 
            +
                  }
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                  @jobs << job
         | 
| 140 | 
            +
                end
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                def prepare_options(options)
         | 
| 143 | 
            +
                  @default_stage = CapistranoMulticonfigParallel.configuration.development_stages.present? ? CapistranoMulticonfigParallel.configuration.development_stages.first : 'development'
         | 
| 144 | 
            +
                  @stage = @stage.present? ? @stage : @default_stage
         | 
| 145 | 
            +
                  options = options.stringify_keys
         | 
| 146 | 
            +
                  options['app'] = options.fetch('app', @application.to_s.clone)
         | 
| 147 | 
            +
                  options['action'] = options.fetch('action', @name.to_s.clone)
         | 
| 148 | 
            +
                  options['stage'] = options.fetch('stage', @stage.to_s.clone)
         | 
| 149 | 
            +
                  options['env_options'] = options.fetch('env_options', @argv.clone)
         | 
| 150 | 
            +
                  options['task_arguments'] = options.fetch('task_arguments', @args.clone)
         | 
| 151 | 
            +
                  options
         | 
| 152 | 
            +
                end
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                def parse_inputted_value(options = {})
         | 
| 155 | 
            +
                  options = options.stringify_keys
         | 
| 156 | 
            +
                  value = options['value'].present? ? options['value'] : fetch(options.fetch('key', :app_branch_name))
         | 
| 157 | 
            +
                  if value.present?
         | 
| 158 | 
            +
                    branch = value.gsub("\r\n", '')
         | 
| 159 | 
            +
                    branch = branch.gsub("\n", '') if branch.present?
         | 
| 160 | 
            +
                    branch = branch.gsub(/\s+/, ' ') if branch.present?
         | 
| 161 | 
            +
                    branch = branch.strip if branch.present?
         | 
| 162 | 
            +
                    return branch
         | 
| 163 | 
            +
                  else
         | 
| 164 | 
            +
                    return ''
         | 
| 165 | 
            +
                  end
         | 
| 166 | 
            +
                end
         | 
| 167 | 
            +
             | 
| 168 | 
            +
                def get_applications_branch_from_stdin(app, app_message)
         | 
| 169 | 
            +
                  app_name = (app.is_a?(Hash) && app[:app].present?) ? app[:app].camelcase : app
         | 
| 170 | 
            +
                  app_name = app_name.present? ? app_name :  "current application"
         | 
| 171 | 
            +
                  message = "Branch name for  #{app_name} for #{app_message} :"
         | 
| 172 | 
            +
                  branch = ''
         | 
| 173 | 
            +
                  if @argv['BRANCH'].blank? || (@argv['BRANCH'].present? && !custom_command?)
         | 
| 174 | 
            +
                    set :app_branch_name, ask(message, nil)
         | 
| 175 | 
            +
                    branch = parse_inputted_value
         | 
| 176 | 
            +
                  else
         | 
| 177 | 
            +
                    branch = @argv['BRANCH']
         | 
| 178 | 
            +
                  end
         | 
| 179 | 
            +
                  branch
         | 
| 180 | 
            +
                end
         | 
| 181 | 
            +
                
         | 
| 182 | 
            +
                def get_app_additional_env_options(app, app_message)
         | 
| 183 | 
            +
                  app_name = (app.is_a?(Hash) && app[:app].present?) ? app[:app].camelcase : app
         | 
| 184 | 
            +
                  app_name = app_name.present? ? app_name :  "current application"
         | 
| 185 | 
            +
                  message = "Please write additional ENV options for #{app_name} for #{app_message} :"
         | 
| 186 | 
            +
                  set :app_additional_env_options, ask_confirm(message, nil)
         | 
| 187 | 
            +
                  fetch_app_additional_env_options
         | 
| 188 | 
            +
                end
         | 
| 189 | 
            +
             | 
| 190 | 
            +
                def fetch_app_additional_env_options
         | 
| 191 | 
            +
                  options = {}
         | 
| 192 | 
            +
                  return options if fetch(:app_additional_env_options).blank?
         | 
| 193 | 
            +
                  env_options = parse_inputted_value(key: :app_additional_env_options)
         | 
| 194 | 
            +
                  env_options = env_options.split(' ')
         | 
| 195 | 
            +
                  options = multi_fetch_argv(env_options)
         | 
| 196 | 
            +
                  options
         | 
| 197 | 
            +
                end
         | 
| 198 | 
            +
             | 
| 199 | 
            +
                def multi_fetch_argv(args)
         | 
| 200 | 
            +
                  options = {}
         | 
| 201 | 
            +
                  args.each do |arg|
         | 
| 202 | 
            +
                    if arg =~ /^(\w+)=(.*)$/m
         | 
| 203 | 
            +
                      options[Regexp.last_match(1)] = Regexp.last_match(2)
         | 
| 204 | 
            +
                    end
         | 
| 205 | 
            +
                  end
         | 
| 206 | 
            +
                  options
         | 
| 207 | 
            +
                end
         | 
| 208 | 
            +
             | 
| 209 | 
            +
                def execute_on_multiple_boxes(main_box_name, options)
         | 
| 210 | 
            +
                  boxes = parse_inputted_value(value: main_box_name).split(',').compact
         | 
| 211 | 
            +
                  boxes.each do |box_name|
         | 
| 212 | 
            +
                    options['env_options']['BOX'] = box_name
         | 
| 213 | 
            +
                    prepare_job(options)
         | 
| 214 | 
            +
                  end
         | 
| 215 | 
            +
                end
         | 
| 216 | 
            +
              end
         | 
| 217 | 
            +
            end
         | 
| @@ -0,0 +1,84 @@ | |
| 1 | 
            +
            require_relative './base_manager'
         | 
| 2 | 
            +
            require_relative '../multi_app_helpers/dependency_tracker'
         | 
| 3 | 
            +
            module CapistranoMulticonfigParallel
         | 
| 4 | 
            +
              # class used to find application dependencies
         | 
| 5 | 
            +
              class MultiAppManager < CapistranoMulticonfigParallel::BaseManager
         | 
| 6 | 
            +
                include Celluloid
         | 
| 7 | 
            +
                include Celluloid::Logger
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def initialize(cap_app, top_level_tasks, stages)
         | 
| 10 | 
            +
                  super(cap_app, top_level_tasks, stages)
         | 
| 11 | 
            +
                  @dependency_tracker = CapistranoMulticonfigParallel::DependencyTracker.new(Actor.current)
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def run
         | 
| 15 | 
            +
                  options = {}
         | 
| 16 | 
            +
                  if custom_command?
         | 
| 17 | 
            +
                    run_custom_command(options)
         | 
| 18 | 
            +
                  else
         | 
| 19 | 
            +
                    menu_deploy_interactive(options)
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
                  process_jobs
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def run_custom_command(options)
         | 
| 25 | 
            +
                  return unless custom_command?
         | 
| 26 | 
            +
                  CapistranoMulticonfigParallel.interactive_menu = true
         | 
| 27 | 
            +
                  options[:action] = @argv['ACTION'].present? ? @argv['ACTION'].present? : 'deploy'
         | 
| 28 | 
            +
                  action_name = @name
         | 
| 29 | 
            +
                  if action_name == custom_commands[:menu]
         | 
| 30 | 
            +
                    menu_deploy_interactive(options)
         | 
| 31 | 
            +
                  elsif action_name == custom_commands[:stages]
         | 
| 32 | 
            +
                    multi_stage_deploy(options)
         | 
| 33 | 
            +
                  else
         | 
| 34 | 
            +
                    raise "Custom command #{@name} not available for multi apps"
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                def multi_stage_deploy(options)
         | 
| 39 | 
            +
                  stages = fetch_multi_stages
         | 
| 40 | 
            +
                  return if  stages.blank?
         | 
| 41 | 
            +
                  multi_collect_and_run_jobs(options) do |apps, new_options|
         | 
| 42 | 
            +
                    apps.each do |app|
         | 
| 43 | 
            +
                      stages.each do |stage|
         | 
| 44 | 
            +
                        deploy_app(new_options.merge('app' => app, 'stage' => stage))
         | 
| 45 | 
            +
                      end
         | 
| 46 | 
            +
                    end if apps.present?
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
                    
         | 
| 50 | 
            +
                def menu_deploy_interactive(options)
         | 
| 51 | 
            +
                  multi_collect_and_run_jobs(options) do |apps, new_options|
         | 
| 52 | 
            +
                    deploy_multiple_apps(apps, new_options)
         | 
| 53 | 
            +
                    deploy_app(new_options) if !custom_command? && new_options['app'].present?
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
                
         | 
| 57 | 
            +
                private
         | 
| 58 | 
            +
                
         | 
| 59 | 
            +
                 
         | 
| 60 | 
            +
                def multi_collect_and_run_jobs(options = {}, &block)
         | 
| 61 | 
            +
                  collect_jobs(options) do |new_options|
         | 
| 62 | 
            +
                    applications = @dependency_tracker.fetch_apps_needed_for_deployment(new_options['app'], new_options['action'])
         | 
| 63 | 
            +
                    backup_the_branch
         | 
| 64 | 
            +
                    block.call(applications, new_options) if block_given?
         | 
| 65 | 
            +
                  end
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                def backup_the_branch
         | 
| 69 | 
            +
                  return if  custom_command? ||  @argv['BRANCH'].blank?
         | 
| 70 | 
            +
                  @branch_backup = @argv['BRANCH'].to_s
         | 
| 71 | 
            +
                  @argv['BRANCH'] = nil
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
                
         | 
| 74 | 
            +
                def deploy_multiple_apps(applications,  options)
         | 
| 75 | 
            +
                  options = options.stringify_keys
         | 
| 76 | 
            +
                  return unless applications.present?
         | 
| 77 | 
            +
                  applications.each do |app|
         | 
| 78 | 
            +
                    deploy_app(options.merge('app' => app))
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                
         | 
| 83 | 
            +
              end
         | 
| 84 | 
            +
            end
         | 
| @@ -0,0 +1,48 @@ | |
| 1 | 
            +
            require_relative './base_manager'
         | 
| 2 | 
            +
            module CapistranoMulticonfigParallel
         | 
| 3 | 
            +
              # class used to find application dependencies
         | 
| 4 | 
            +
              class SingleAppManager < CapistranoMulticonfigParallel::BaseManager
         | 
| 5 | 
            +
                include Celluloid
         | 
| 6 | 
            +
                include Celluloid::Logger
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                def run
         | 
| 9 | 
            +
                  options = {}
         | 
| 10 | 
            +
                  if custom_command?
         | 
| 11 | 
            +
                    run_custom_command(options)
         | 
| 12 | 
            +
                  else
         | 
| 13 | 
            +
                    deploy_single_app(options)
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                  process_jobs
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def run_custom_command(options)
         | 
| 19 | 
            +
                  return unless custom_command?
         | 
| 20 | 
            +
                  action_name = @name
         | 
| 21 | 
            +
                  if action_name == custom_commands[:stages]
         | 
| 22 | 
            +
                    stage_deploy(options)
         | 
| 23 | 
            +
                  else
         | 
| 24 | 
            +
                    raise "Custom command #{@name} not available for single apps"
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def stage_deploy(options)
         | 
| 29 | 
            +
                   return unless custom_command?
         | 
| 30 | 
            +
                  stages = fetch_multi_stages
         | 
| 31 | 
            +
                  return if  stages.blank?
         | 
| 32 | 
            +
                  collect_jobs(options) do |new_options|
         | 
| 33 | 
            +
                    stages.each do |stage|
         | 
| 34 | 
            +
                      deploy_app(new_options.merge('stage' => stage, 'action' => 'deploy'))
         | 
| 35 | 
            +
                    end
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
                
         | 
| 39 | 
            +
                
         | 
| 40 | 
            +
                def deploy_single_app(options)
         | 
| 41 | 
            +
                  return if custom_command?
         | 
| 42 | 
            +
                  collect_jobs(options) do |new_options|
         | 
| 43 | 
            +
                    deploy_app(new_options) 
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
                
         | 
| 47 | 
            +
              end
         | 
| 48 | 
            +
            end
         | 
| @@ -0,0 +1,40 @@ | |
| 1 | 
            +
            require 'fileutils'
         | 
| 2 | 
            +
            module CapistranoMulticonfigParallel
         | 
| 3 | 
            +
              # class used to find application dependencies
         | 
| 4 | 
            +
              class StandardDeploy
         | 
| 5 | 
            +
                extend FileUtils
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def self.setup_command_line_standard(options)
         | 
| 8 | 
            +
                  opts = ''
         | 
| 9 | 
            +
                  options.each do |key, value|
         | 
| 10 | 
            +
                    opts << "#{key}=#{value}" if value.present?
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
                  opts
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def self.execute_standard_deploy(options)
         | 
| 16 | 
            +
                  app = options.fetch(:app, '')
         | 
| 17 | 
            +
                  stage = options.fetch(:env, 'development')
         | 
| 18 | 
            +
                  action_name = options.fetch(:action, 'deploy')
         | 
| 19 | 
            +
                  action = "#{action_name}[#{options.fetch(:task_arguments, []).join(',')}]"
         | 
| 20 | 
            +
                  arguments = setup_command_line_standard(options.fetch(:env_options, {}))
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  command = "bundle exec cap #{app}:#{stage} #{action}  #{arguments}"
         | 
| 23 | 
            +
                  puts("\n\n\n Executing '#{command}' \n\n\n .")
         | 
| 24 | 
            +
                  sh("#{command}")
         | 
| 25 | 
            +
                rescue => ex
         | 
| 26 | 
            +
                  CapistranoMulticonfigParallel.log_message(ex)
         | 
| 27 | 
            +
                  if @name == 'deploy'
         | 
| 28 | 
            +
                    begin
         | 
| 29 | 
            +
                      action = "deploy:rollback[#{options.fetch(:task_arguments, []).join(',')}]"
         | 
| 30 | 
            +
                      command = "bundle exec cap #{app}:#{stage} #{action} #{arguments}"
         | 
| 31 | 
            +
                      puts("\n\n\n Executing #{command} \n\n\n .")
         | 
| 32 | 
            +
                      sh("#{command}")
         | 
| 33 | 
            +
                    rescue => exception
         | 
| 34 | 
            +
                      CapistranoMulticonfigParallel.log_message(exception)
         | 
| 35 | 
            +
                      # nothing to do if rollback fails
         | 
| 36 | 
            +
                    end
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
            end
         |