capistrano 3.2.1 → 3.3.3

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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +5 -3
  3. data/CHANGELOG.md +92 -2
  4. data/Gemfile +1 -5
  5. data/README.md +84 -3
  6. data/Rakefile +5 -1
  7. data/capistrano.gemspec +5 -1
  8. data/features/configuration.feature +9 -7
  9. data/features/deploy.feature +9 -8
  10. data/features/step_definitions/assertions.rb +15 -17
  11. data/features/step_definitions/cap_commands.rb +1 -1
  12. data/features/step_definitions/setup.rb +11 -7
  13. data/features/support/env.rb +8 -9
  14. data/features/support/remote_command_helpers.rb +2 -3
  15. data/features/support/vagrant_helpers.rb +35 -0
  16. data/lib/capistrano/all.rb +2 -1
  17. data/lib/capistrano/application.rb +52 -7
  18. data/lib/capistrano/configuration.rb +39 -10
  19. data/lib/capistrano/configuration/filter.rb +56 -0
  20. data/lib/capistrano/configuration/question.rb +23 -11
  21. data/lib/capistrano/configuration/server.rb +14 -5
  22. data/lib/capistrano/configuration/servers.rb +12 -29
  23. data/lib/capistrano/defaults.rb +11 -9
  24. data/lib/capistrano/deploy.rb +1 -0
  25. data/lib/capistrano/dsl.rb +13 -2
  26. data/lib/capistrano/dsl/env.rb +6 -2
  27. data/lib/capistrano/dsl/task_enhancements.rb +5 -3
  28. data/lib/capistrano/git.rb +8 -2
  29. data/lib/capistrano/hg.rb +7 -1
  30. data/lib/capistrano/svn.rb +2 -2
  31. data/lib/capistrano/tasks/deploy.rake +12 -10
  32. data/lib/capistrano/tasks/git.rake +1 -1
  33. data/lib/capistrano/tasks/install.rake +17 -14
  34. data/lib/capistrano/templates/Capfile +6 -4
  35. data/lib/capistrano/templates/deploy.rb.erb +5 -15
  36. data/lib/capistrano/upload_task.rb +9 -0
  37. data/lib/capistrano/version.rb +1 -1
  38. data/spec/integration/dsl_spec.rb +129 -10
  39. data/spec/lib/capistrano/application_spec.rb +24 -6
  40. data/spec/lib/capistrano/configuration/filter_spec.rb +105 -0
  41. data/spec/lib/capistrano/configuration/question_spec.rb +18 -12
  42. data/spec/lib/capistrano/configuration/server_spec.rb +19 -19
  43. data/spec/lib/capistrano/configuration/servers_spec.rb +101 -20
  44. data/spec/lib/capistrano/configuration_spec.rb +24 -3
  45. data/spec/lib/capistrano/dsl/task_enhancements_spec.rb +88 -0
  46. data/spec/lib/capistrano/dsl_spec.rb +2 -13
  47. data/spec/lib/capistrano/git_spec.rb +15 -4
  48. data/spec/lib/capistrano/hg_spec.rb +13 -2
  49. data/spec/lib/capistrano/scm_spec.rb +3 -3
  50. data/spec/lib/capistrano/svn_spec.rb +11 -1
  51. data/spec/lib/capistrano/upload_task_spec.rb +19 -0
  52. data/spec/lib/capistrano/version_validator_spec.rb +4 -4
  53. data/spec/spec_helper.rb +2 -1
  54. data/spec/support/Vagrantfile +1 -1
  55. data/spec/support/test_app.rb +2 -0
  56. metadata +45 -26
  57. data/lib/capistrano/configuration/servers/host_filter.rb +0 -82
  58. data/lib/capistrano/configuration/servers/role_filter.rb +0 -86
  59. data/spec/lib/capistrano/configuration/servers/host_filter_spec.rb +0 -84
  60. data/spec/lib/capistrano/configuration/servers/role_filter_spec.rb +0 -140
@@ -1,5 +1,5 @@
1
1
  When(/^I run cap "(.*?)"$/) do |task|
2
- @success = TestApp.cap(task)
2
+ @success, @output = TestApp.cap(task)
3
3
  end
4
4
 
5
5
  When(/^I run cap "(.*?)" as part of a release$/) do |task|
@@ -6,17 +6,21 @@ Given(/^servers with the roles app and web$/) do
6
6
  vagrant_cli_command('up') rescue nil
7
7
  end
8
8
 
9
- Given(/^a required file$/) do
9
+ Given(/^a linked file "(.*?)"$/) do |file|
10
+ # ignoring other linked files
11
+ TestApp.append_to_deploy_file("set :linked_files, ['#{file}']")
10
12
  end
11
13
 
12
- Given(/^that file exists$/) do
13
- run_vagrant_command("touch #{TestApp.linked_file}")
14
+ Given(/^file "(.*?)" exists in shared path$/) do |file|
15
+ file_shared_path = TestApp.shared_path.join(file)
16
+ run_vagrant_command("mkdir -p #{TestApp.shared_path}")
17
+ run_vagrant_command("touch #{file_shared_path}")
14
18
  end
15
19
 
16
- Given(/^the file does not exist$/) do
17
- pending
18
- file = TestApp.linked_file
19
- run_vagrant_command("[ -f #{file} ] && rm #{file}")
20
+ Given(/^file "(.*?)" does not exist in shared path$/) do |file|
21
+ file_shared_path = TestApp.shared_path.join(file)
22
+ run_vagrant_command("mkdir -p #{TestApp.shared_path}")
23
+ run_vagrant_command("touch #{file_shared_path} && rm #{file_shared_path}")
20
24
  end
21
25
 
22
26
  Given(/^a custom task to generate a file$/) do
@@ -1,12 +1,11 @@
1
- require 'kuroko'
2
-
3
- project_root = File.expand_path('../../../', __FILE__)
4
- vagrant_root = File.join(project_root, 'spec/support')
5
-
6
- Kuroko.configure do |config|
7
- config.vagrant_root = 'spec/support'
1
+ PROJECT_ROOT = File.expand_path('../../../', __FILE__)
2
+ VAGRANT_ROOT = File.join(PROJECT_ROOT, 'spec/support')
3
+ VAGRANT_BIN = ENV['VAGRANT_BIN'] || "vagrant"
4
+
5
+ at_exit do
6
+ if ENV['KEEP_RUNNING']
7
+ VagrantHelpers.run_vagrant_command("rm -rf /home/vagrant/var")
8
+ end
8
9
  end
9
10
 
10
- puts vagrant_root.inspect
11
-
12
11
  require_relative '../../spec/support/test_app'
@@ -1,5 +1,4 @@
1
1
  module RemoteCommandHelpers
2
-
3
2
  def test_dir_exists(path)
4
3
  exists?('d', path)
5
4
  end
@@ -13,11 +12,11 @@ module RemoteCommandHelpers
13
12
  end
14
13
 
15
14
  def exists?(type, path)
16
- %{[ -#{type} "#{path}" ] && echo "#{path} exists." || echo "Error: #{path} does not exist."}
15
+ %{[ -#{type} "#{path}" ]}
17
16
  end
18
17
 
19
18
  def safely_remove_file(path)
20
- run_vagrant_command("rm #{test_file}") rescue Vagrant::Errors::VagrantError
19
+ run_vagrant_command("rm #{test_file}") rescue VagrantHelpers::VagrantSSHCommandError
21
20
  end
22
21
  end
23
22
 
@@ -0,0 +1,35 @@
1
+ module VagrantHelpers
2
+ extend self
3
+
4
+ class VagrantSSHCommandError < RuntimeError; end
5
+
6
+ at_exit do
7
+ if ENV['KEEP_RUNNING']
8
+ puts "Vagrant vm will be left up because KEEP_RUNNING is set."
9
+ puts "Rerun without KEEP_RUNNING set to cleanup the vm."
10
+ else
11
+ vagrant_cli_command("destroy -f")
12
+ end
13
+ end
14
+
15
+ def vagrant_cli_command(command)
16
+ puts "[vagrant] #{command}"
17
+ Dir.chdir(VAGRANT_ROOT) do
18
+ `#{VAGRANT_BIN} #{command} 2>&1`.split("\n").each do |line|
19
+ puts "[vagrant] #{line}"
20
+ end
21
+ end
22
+ $?
23
+ end
24
+
25
+ def run_vagrant_command(command)
26
+ if (status = vagrant_cli_command("ssh -c #{command.inspect}")).success?
27
+ true
28
+ else
29
+ fail VagrantSSHCommandError, status
30
+ end
31
+ end
32
+
33
+ end
34
+
35
+ World(VagrantHelpers)
@@ -1,6 +1,7 @@
1
1
  require 'rake'
2
2
  require 'sshkit'
3
- require 'sshkit/dsl'
3
+
4
+ require 'io/console'
4
5
 
5
6
  Rake.application.options.trace = true
6
7
 
@@ -16,10 +16,43 @@ module Capistrano
16
16
  end
17
17
 
18
18
  def sort_options(options)
19
- options.push(version, roles, dry_run, hostfilter)
20
- super
19
+ not_applicable_to_capistrano = %w(quiet silent verbose)
20
+ options.reject! do |(switch, *)|
21
+ switch =~ /--#{Regexp.union(not_applicable_to_capistrano)}/
22
+ end
23
+
24
+ super.push(version, dry_run, roles, hostfilter)
25
+ end
26
+
27
+ def handle_options
28
+ options.rakelib = ['rakelib']
29
+ options.trace_output = $stderr
30
+
31
+ OptionParser.new do |opts|
32
+ opts.banner = "See full documentation at http://capistranorb.com/."
33
+ opts.separator ""
34
+ opts.separator "Install capistrano in a project:"
35
+ opts.separator " bundle exec cap install [STAGES=qa,staging,production,...]"
36
+ opts.separator ""
37
+ opts.separator "Show available tasks:"
38
+ opts.separator " bundle exec cap -T"
39
+ opts.separator ""
40
+ opts.separator "Invoke (or simulate invoking) a task:"
41
+ opts.separator " bundle exec cap [--dry-run] STAGE TASK"
42
+ opts.separator ""
43
+ opts.separator "Advanced options:"
44
+
45
+ opts.on_tail("-h", "--help", "-H", "Display this help message.") do
46
+ puts opts
47
+ exit
48
+ end
49
+
50
+ standard_rake_options.each { |args| opts.on(*args) }
51
+ opts.environment('RAKEOPT')
52
+ end.parse!
21
53
  end
22
54
 
55
+
23
56
  def top_level_tasks
24
57
  if tasks_without_stage_dependency.include?(@top_level_tasks.first)
25
58
  @top_level_tasks
@@ -28,6 +61,18 @@ module Capistrano
28
61
  end
29
62
  end
30
63
 
64
+ def display_error_message(ex)
65
+ unless options.backtrace
66
+ if loc = Rake.application.find_rakefile_location
67
+ whitelist = (@imported.dup << loc[0]).map{|f| File.absolute_path(f, loc[1])}
68
+ pattern = %r@^(?!#{whitelist.map{|p| Regexp.quote(p)}.join('|')})@
69
+ Rake.application.options.suppress_backtrace_pattern = pattern
70
+ end
71
+ trace "(Backtrace restricted to imported tasks)"
72
+ end
73
+ super
74
+ end
75
+
31
76
  def exit_because_of_exception(ex)
32
77
  if respond_to?(:deploying?) && deploying?
33
78
  exit_deploy_because_of_exception(ex)
@@ -42,7 +87,7 @@ module Capistrano
42
87
  if options.show_tasks
43
88
  invoke 'load:defaults'
44
89
  set(:stage, '')
45
- Dir[deploy_config_path, stage_definitions].each { |f| add_import f }
90
+ Dir[deploy_config_path].each { |f| add_import f }
46
91
  end
47
92
 
48
93
  super
@@ -74,18 +119,18 @@ module Capistrano
74
119
 
75
120
  def roles
76
121
  ['--roles ROLES', '-r',
77
- "Filter command to only apply to these roles (separate multiple roles with a comma)",
122
+ "Run SSH commands only on hosts matching these roles",
78
123
  lambda { |value|
79
- Configuration.env.set(:filter, roles: value.split(","))
124
+ Configuration.env.add_cmdline_filter(:role, value)
80
125
  }
81
126
  ]
82
127
  end
83
128
 
84
129
  def hostfilter
85
130
  ['--hosts HOSTS', '-z',
86
- "Filter command to only apply to these hosts (separate multiple hosts with a comma)",
131
+ "Run SSH commands only on matching hosts",
87
132
  lambda { |value|
88
- Configuration.env.set(:filter, hosts: value.split(","))
133
+ Configuration.env.add_cmdline_filter(:host, value)
89
134
  }
90
135
  ]
91
136
  end
@@ -1,22 +1,25 @@
1
+ require_relative 'configuration/filter'
1
2
  require_relative 'configuration/question'
2
- require_relative 'configuration/servers'
3
3
  require_relative 'configuration/server'
4
+ require_relative 'configuration/servers'
4
5
 
5
6
  module Capistrano
6
7
  class Configuration
7
8
 
8
- class << self
9
- def env
10
- @env ||= new
11
- end
9
+ def initialize(config = nil)
10
+ @config ||= config
11
+ end
12
12
 
13
- def reset!
14
- @env = new
15
- end
13
+ def self.env
14
+ @env ||= new
16
15
  end
17
16
 
18
- def ask(key, default=nil)
19
- question = Question.new(self, key, default)
17
+ def self.reset!
18
+ @env = new
19
+ end
20
+
21
+ def ask(key, default=nil, options={})
22
+ question = Question.new(key, default, options)
20
23
  set(key, question)
21
24
  end
22
25
 
@@ -24,6 +27,10 @@ module Capistrano
24
27
  config[key] = value
25
28
  end
26
29
 
30
+ def set_if_empty(key, value)
31
+ config[key] = value unless config.has_key? key
32
+ end
33
+
27
34
  def delete(key)
28
35
  config.delete(key)
29
36
  end
@@ -84,8 +91,30 @@ module Capistrano
84
91
  @timestamp ||= Time.now.utc
85
92
  end
86
93
 
94
+ def setup_filters
95
+ @filters = cmdline_filters.clone
96
+ @filters << Filter.new(:role, ENV['ROLES']) if ENV['ROLES']
97
+ @filters << Filter.new(:host, ENV['HOSTS']) if ENV['HOSTS']
98
+ fh = fetch_for(:filter,{})
99
+ @filters << Filter.new(:host, fh[:host]) if fh[:host]
100
+ @filters << Filter.new(:role, fh[:role]) if fh[:role]
101
+ end
102
+
103
+ def add_cmdline_filter(type, values)
104
+ cmdline_filters << Filter.new(type, values)
105
+ end
106
+
107
+ def filter list
108
+ setup_filters if @filters.nil?
109
+ @filters.reduce(list) { |l,f| f.filter l }
110
+ end
111
+
87
112
  private
88
113
 
114
+ def cmdline_filters
115
+ @cmdline_filters ||= []
116
+ end
117
+
89
118
  def servers
90
119
  @servers ||= Servers.new
91
120
  end
@@ -0,0 +1,56 @@
1
+ require 'capistrano/configuration'
2
+
3
+ module Capistrano
4
+ class Configuration
5
+ class Filter
6
+ def initialize type, values = nil
7
+ raise "Invalid filter type #{type}" unless [:host,:role].include? type
8
+ av = Array(values)
9
+ @mode = case
10
+ when av.size == 0 then :none
11
+ when av.include?(:all) then :all
12
+ else type
13
+ end
14
+ @rex = case @mode
15
+ when :host
16
+ av.map!{|v| (v.is_a?(String) && v =~ /^(?<name>[-A-Za-z0-9.]+)(,\g<name>)*$/) ? v.split(',') : v }
17
+ av.flatten!
18
+ av.map! do |v|
19
+ case v
20
+ when Regexp then v
21
+ else
22
+ vs = v.to_s
23
+ vs =~ /^[-A-Za-z0-9.]+$/ ? vs : Regexp.new(vs)
24
+ end
25
+ end
26
+ Regexp.union av
27
+ when :role
28
+ av.map!{|v| v.is_a?(String) ? v.split(',') : v }
29
+ av.flatten!
30
+ av.map! do |v|
31
+ case v
32
+ when Regexp then v
33
+ else
34
+ vs = v.to_s
35
+ vs =~ %r{^/(.+)/$} ? Regexp.new($1) : vs
36
+ end
37
+ end
38
+ Regexp.union av
39
+ else
40
+ nil
41
+ end
42
+ end
43
+ def filter servers
44
+ as = Array(servers)
45
+ case @mode
46
+ when :none then return []
47
+ when :all then return servers
48
+ when :host
49
+ as.select {|s| @rex.match s.hostname}
50
+ when :role
51
+ as.select {|s| s.roles.any? {|r| @rex.match r} }
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -2,27 +2,23 @@ module Capistrano
2
2
  class Configuration
3
3
  class Question
4
4
 
5
- def initialize(env, key, default)
6
- @env, @key, @default = env, key, default
5
+ def initialize(key, default, options = {})
6
+ @key, @default, @options = key, default, options
7
7
  end
8
8
 
9
9
  def call
10
10
  ask_question
11
- save_response
11
+ value_or_default
12
12
  end
13
13
 
14
14
  private
15
- attr_reader :env, :key, :default
15
+ attr_reader :key, :default, :options
16
16
 
17
17
  def ask_question
18
18
  $stdout.print question
19
19
  end
20
20
 
21
- def save_response
22
- env.set(key, value)
23
- end
24
-
25
- def value
21
+ def value_or_default
26
22
  if response.empty?
27
23
  default
28
24
  else
@@ -31,12 +27,28 @@ module Capistrano
31
27
  end
32
28
 
33
29
  def response
34
- @response ||= $stdin.gets.chomp
30
+ return @response if defined? @response
31
+
32
+ @response = (gets || "").chomp
35
33
  end
36
-
34
+
35
+ def gets
36
+ if echo?
37
+ $stdin.gets
38
+ else
39
+ $stdin.noecho(&:gets).tap{ $stdout.print "\n" }
40
+ end
41
+ rescue Errno::EIO
42
+ # when stdio gets closed
43
+ end
44
+
37
45
  def question
38
46
  I18n.t(:question, key: key, default_value: default, scope: :capistrano)
39
47
  end
48
+
49
+ def echo?
50
+ (options || {}).fetch(:echo, true)
51
+ end
40
52
  end
41
53
  end
42
54
  end
@@ -11,11 +11,13 @@ module Capistrano
11
11
 
12
12
  def add_roles(roles)
13
13
  Array(roles).each { |role| add_role(role) }
14
+ self
14
15
  end
15
16
  alias roles= add_roles
16
17
 
17
18
  def add_role(role)
18
19
  roles.add role.to_sym
20
+ self
19
21
  end
20
22
 
21
23
  def has_role?(role)
@@ -40,11 +42,9 @@ module Capistrano
40
42
  @properties ||= Properties.new
41
43
  end
42
44
 
43
- def netssh_options_with_options
44
- @netssh_options ||= netssh_options_without_options.merge( fetch(:ssh_options) || {} )
45
+ def netssh_options
46
+ @netssh_options ||= super.merge( fetch(:ssh_options) || {} )
45
47
  end
46
- alias_method :netssh_options_without_options, :netssh_options
47
- alias_method :netssh_options, :netssh_options_with_options
48
48
 
49
49
  def roles_array
50
50
  roles.to_a
@@ -71,7 +71,16 @@ module Capistrano
71
71
  end
72
72
 
73
73
  def set(key, value)
74
- @properties[key] = value
74
+ pval = @properties[key]
75
+ if pval.is_a? Hash and value.is_a? Hash
76
+ pval.merge!(value)
77
+ elsif pval.is_a? Set and value.is_a? Set
78
+ pval.merge(value)
79
+ elsif pval.is_a? Array and value.is_a? Array
80
+ pval.concat value
81
+ else
82
+ @properties[key] = value
83
+ end
75
84
  end
76
85
 
77
86
  def fetch(key)