capistrano 3.2.1 → 3.3.3

Sign up to get free protection for your applications and to get access to all the features.
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)