├── .gitignore ├── Gemfile ├── Rakefile ├── LICENSE ├── capistrano-hostmenu.gemspec ├── lib └── capistrano │ ├── tasks │ └── hostmenu.rake │ └── hostmenu.rb ├── Gemfile.lock └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require 'rspec/core/rake_task' 3 | require 'yard' 4 | 5 | RSpec::Core::RakeTask.new(:spec) 6 | 7 | task :default => :spec 8 | 9 | 10 | # Generate documentation 11 | YARD::Rake::YardocTask.new(:doc) do |t| 12 | #t.files = %w{lib/hsf.rb} 13 | end 14 | 15 | ## Developping tasks 16 | require "rake/extensiontask" 17 | 18 | # `rake compile` to compile 19 | Rake::ExtensionTask.new "hsf" do |ext| 20 | ext.name = "hsf_cpp" 21 | end 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 qhwa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /capistrano-hostmenu.gemspec: -------------------------------------------------------------------------------- 1 | # Ensure we require the local version and not one we might have installed already 2 | spec = Gem::Specification.new do |s| 3 | 4 | s.name = "capistrano-hostmenu" 5 | s.version = '0.2.0' 6 | s.summary = "Capistrano plugin that prompt a text menu to choose target host." 7 | s.author = "qhwa" 8 | s.email = "qhwa@163.com" 9 | s.homepage = "https://github.com/qhwa/capistrano-hostmenu" 10 | s.has_rdoc = false 11 | s.extra_rdoc_files = %w(README.md) 12 | s.rdoc_options = %w(--main README.md) 13 | s.files = %w(README.md Rakefile Gemfile.lock Gemfile) + 14 | Dir.glob("{bin,lib}/**/*") - 15 | Dir.glob("spec/**/*") 16 | 17 | s.require_paths << 'lib' 18 | 19 | s.add_runtime_dependency('capistrano','>= 3.0.0') 20 | s.add_runtime_dependency('colored', '> 0') 21 | 22 | s.add_development_dependency("rake") 23 | s.add_development_dependency("rake-compiler") 24 | s.add_development_dependency("bundler") 25 | s.add_development_dependency("yard") 26 | s.add_development_dependency("rspec") 27 | 28 | end 29 | -------------------------------------------------------------------------------- /lib/capistrano/tasks/hostmenu.rake: -------------------------------------------------------------------------------- 1 | namespace :deploy do 2 | 3 | desc 'print environment variables' 4 | task :info do 5 | role = fetch(:host_menu_role_to_filter) 6 | 7 | puts "--" * 50 8 | puts "About to deploy, check your seatbelt~" 9 | puts "env: #{fetch(fetch(:host_menu_env_key)).to_s.bold.blue}" 10 | puts "branch: #{fetch(:branch).to_s.bold.green}" 11 | 12 | servers = Capistrano::Configuration.env.filter(roles(role)) 13 | puts "server: #{servers.map(&:hostname).join("\n ").red}" 14 | puts "--" * 50 15 | end 16 | 17 | desc <<-DESC 18 | Prompt a text based list menu for user to select. \ 19 | Following changes will only be applied on selected servers. 20 | 21 | User will see menu like this: 22 | 23 | ~~~ 24 | Please select target host(s): 25 | [1] my.example1.com 26 | [2] my.example2.com 27 | [3] all (default) 28 | Please enter host_numbers (3): 29 | ~~~ 30 | DESC 31 | task :host_menu do 32 | next if fetch(:show_host_menu) == false 33 | 34 | Capistrano::Hostmenu.new 35 | invoke 'deploy:info' unless fetch(:host_menu_show_info_after_select) == false 36 | end 37 | end 38 | 39 | 40 | Capistrano::Hostmenu.set_default_config 41 | 42 | Capistrano::DSL.stages.each do |stage| 43 | after stage, 'deploy:host_menu' 44 | end 45 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | capistrano-hostmenu (0.1.3) 5 | capistrano (>= 3.0.0) 6 | colored (> 0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | airbrussh (1.3.0) 12 | sshkit (>= 1.6.1, != 1.7.0) 13 | capistrano (3.10.2) 14 | airbrussh (>= 1.0.0) 15 | i18n 16 | rake (>= 10.0.0) 17 | sshkit (>= 1.9.0) 18 | colored (1.2) 19 | concurrent-ruby (1.0.5) 20 | diff-lcs (1.3) 21 | i18n (1.0.1) 22 | concurrent-ruby (~> 1.0) 23 | net-scp (1.2.1) 24 | net-ssh (>= 2.6.5) 25 | net-ssh (5.0.0) 26 | rake (12.3.1) 27 | rake-compiler (1.0.4) 28 | rake 29 | rspec (3.7.0) 30 | rspec-core (~> 3.7.0) 31 | rspec-expectations (~> 3.7.0) 32 | rspec-mocks (~> 3.7.0) 33 | rspec-core (3.7.1) 34 | rspec-support (~> 3.7.0) 35 | rspec-expectations (3.7.0) 36 | diff-lcs (>= 1.2.0, < 2.0) 37 | rspec-support (~> 3.7.0) 38 | rspec-mocks (3.7.0) 39 | diff-lcs (>= 1.2.0, < 2.0) 40 | rspec-support (~> 3.7.0) 41 | rspec-support (3.7.1) 42 | sshkit (1.16.1) 43 | net-scp (>= 1.1.2) 44 | net-ssh (>= 2.8.0) 45 | yard (0.9.13) 46 | 47 | PLATFORMS 48 | ruby 49 | 50 | DEPENDENCIES 51 | bundler 52 | capistrano-hostmenu! 53 | rake 54 | rake-compiler 55 | rspec 56 | yard 57 | 58 | BUNDLED WITH 59 | 1.11.2 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # capistrano-hostmenu 2 | A capistrano plugin which allows you to choose one or more server to deploy via a text menu. 3 | 4 | Here's an example: 5 | 6 | In you deploy config: 7 | 8 | ~~~ruby 9 | server 'example1.com', user: "#{fetch(:deploy_user)}", roles: %w{web app} 10 | server 'example2.com', user: "#{fetch(:deploy_user)}", roles: %w{web app} 11 | ~~~ 12 | 13 | when you run `cap deploy` or `cap deploy:start` or other deploy tasks, you will see a menu: 14 | 15 | ~~~sh 16 | Please choose which server(s) to deploy: 17 | [1] example1.com 18 | [2] example2.com 19 | [3] all (default) 20 | Please enter host_numbers (3): 21 | ~~~ 22 | 23 | you can answer with: 24 | 25 | * `1` for choosing server `example1.com`, 26 | * `2` for `example2.com`, 27 | * `3` or `1,2` for both. 28 | 29 | ## Installation 30 | 31 | ~~~sh 32 | gem install capistrano-hostmenu 33 | ~~~ 34 | 35 | or put this in your `Gemfile` then run `bundle install`: 36 | 37 | ~~~ruby 38 | gem 'capistrano-hostmenu', require: false 39 | ~~~ 40 | 41 | After the gem is installed, put this in your `Capfile`: 42 | 43 | ~~~ruby 44 | require 'capistrano/hostmenu' 45 | ~~~ 46 | 47 | Then you will see host selecting menu any time before deploying. 48 | 49 | ## Configurations 50 | 51 | set these variables in your deploy config (commonly `deploy.rb`) 52 | 53 | ~~~ruby 54 | set :host_menu_default_selection, :all # or :first, 1 55 | set :host_menu_prompt_msg, 'Please choose which server(s) to deploy:'.blue 56 | set :host_menu_caption_of_all, 'all' 57 | set :host_menu_caption_of_default, '(default)' 58 | set :host_menu_invalid_range_msg, 'Please provide a number in (1..%d)'.red 59 | set :host_menu_invalid_multi_choose_msg, 'Do you mean to choose all servers?'.red 60 | set :host_menu_role_to_filter, :all 61 | set :host_menu_env_key, :rails_env 62 | ~~~ 63 | -------------------------------------------------------------------------------- /lib/capistrano/hostmenu.rb: -------------------------------------------------------------------------------- 1 | require 'colored' 2 | 3 | module Capistrano 4 | 5 | class Hostmenu 6 | 7 | include ::Capistrano::DSL 8 | 9 | def self.set_default_config 10 | set :host_menu_prompt_msg, 'Please choose which server(s) to deploy:'.blue 11 | set :host_menu_default_selection, :all # or :first, 1 12 | set :host_menu_caption_of_all, 'all' 13 | set :host_menu_caption_of_default, '(default)' 14 | set :host_menu_invalid_range_msg, 'Please provide a number in (1..%d)'.red 15 | set :host_menu_invalid_multi_choose_msg, 'Do you mean to choose all servers?'.red 16 | set :host_menu_role_to_filter, :all 17 | set :host_menu_env_key, :rails_env 18 | end 19 | 20 | def initialize 21 | if deploy_hosts.size > 1 22 | prompt_menu default: fetch(:host_menu_default_selection) 23 | end 24 | end 25 | 26 | def prompt_menu default: :all 27 | puts fetch(:host_menu_prompt_msg) 28 | 29 | default = input_for(default) 30 | default_cap = fetch(:host_menu_caption_of_default) 31 | 32 | (deploy_hosts + [fetch(:host_menu_caption_of_all)]).each_with_index do |host, i| 33 | cap = if i == default - 1 34 | "[%d] %s %s" % [i+1, host, default_cap] 35 | else 36 | "[%d] %s" % [i+1, host] 37 | end 38 | puts " " << cap.green 39 | end 40 | 41 | ask :host_numbers, default.to_s 42 | set_hosts 43 | end 44 | 45 | private 46 | 47 | def input_for selection 48 | case selection 49 | when :all 50 | max_selection 51 | when 0 52 | 1 53 | when Fixnum 54 | selection 55 | else 56 | 1 57 | end 58 | end 59 | 60 | def max_selection 61 | deploy_hosts.size + 1 62 | end 63 | 64 | def deploy_hosts 65 | release_roles(fetch(:host_menu_role_to_filter)) 66 | end 67 | 68 | def selection_for_all 69 | max_selection 70 | end 71 | 72 | def set_hosts 73 | ids = fetch(:host_numbers).split(/\s*,\s*/).map(&:to_i).uniq 74 | unless ids.all? {|i| (1..max_selection).include? i} 75 | puts fetch(:host_menu_invalid_range_msg) % max_selection 76 | exit 1 77 | end 78 | 79 | if ids.size > 1 && ids.include?(selection_for_all) 80 | puts fetch(:host_menu_invalid_multi_choose_msg) 81 | exit 1 82 | end 83 | 84 | unless ids.include?(selection_for_all) 85 | set_host_filter ids.map {|i| deploy_hosts[i-1].hostname} 86 | end 87 | end 88 | 89 | def set_host_filter hosts 90 | if defined? Capistrano::Configuration::Servers::HostFilter 91 | set :filter, hosts: hosts 92 | else 93 | role = fetch(:host_menu_role_to_filter) 94 | Capistrano::Configuration.env.add_filter Filter.new(hosts, role) 95 | end 96 | end 97 | 98 | 99 | class Filter < Struct.new(:hosts, :role) 100 | def filter srv 101 | if Array === srv 102 | return srv.select {|s| filter(s)} 103 | end 104 | 105 | if role != :all and not srv.roles.include?(role) 106 | return srv 107 | end 108 | 109 | if hosts.include? srv.hostname 110 | return srv 111 | end 112 | 113 | return nil 114 | end 115 | end 116 | 117 | end 118 | end 119 | 120 | load File.expand_path('../tasks/hostmenu.rake', __FILE__) 121 | 122 | --------------------------------------------------------------------------------