capistrano 3.5.0 → 3.6.0

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +5 -1
  3. data/CHANGELOG.md +55 -10
  4. data/README.md +3 -3
  5. data/RELEASING.md +1 -0
  6. data/UPGRADING-3.7.md +97 -0
  7. data/capistrano.gemspec +1 -1
  8. data/features/deploy.feature +1 -0
  9. data/features/stage_failure.feature +9 -0
  10. data/features/step_definitions/assertions.rb +5 -0
  11. data/features/step_definitions/setup.rb +4 -0
  12. data/lib/capistrano/application.rb +5 -10
  13. data/lib/capistrano/configuration.rb +8 -7
  14. data/lib/capistrano/configuration/filter.rb +4 -5
  15. data/lib/capistrano/configuration/host_filter.rb +1 -1
  16. data/lib/capistrano/configuration/plugin_installer.rb +1 -1
  17. data/lib/capistrano/configuration/server.rb +8 -2
  18. data/lib/capistrano/configuration/validated_variables.rb +75 -0
  19. data/lib/capistrano/configuration/variables.rb +7 -23
  20. data/lib/capistrano/defaults.rb +10 -0
  21. data/lib/capistrano/doctor.rb +1 -0
  22. data/lib/capistrano/doctor/gems_doctor.rb +1 -1
  23. data/lib/capistrano/doctor/servers_doctor.rb +105 -0
  24. data/lib/capistrano/doctor/variables_doctor.rb +6 -7
  25. data/lib/capistrano/dsl.rb +28 -4
  26. data/lib/capistrano/dsl/paths.rb +1 -1
  27. data/lib/capistrano/dsl/stages.rb +15 -1
  28. data/lib/capistrano/dsl/task_enhancements.rb +6 -1
  29. data/lib/capistrano/i18n.rb +2 -0
  30. data/lib/capistrano/proc_helpers.rb +13 -0
  31. data/lib/capistrano/tasks/deploy.rake +9 -1
  32. data/lib/capistrano/tasks/doctor.rake +6 -1
  33. data/lib/capistrano/tasks/git.rake +11 -4
  34. data/lib/capistrano/templates/deploy.rb.erb +2 -15
  35. data/lib/capistrano/version.rb +1 -1
  36. data/spec/lib/capistrano/configuration/empty_filter_spec.rb +1 -1
  37. data/spec/lib/capistrano/configuration/filter_spec.rb +8 -8
  38. data/spec/lib/capistrano/configuration/host_filter_spec.rb +1 -1
  39. data/spec/lib/capistrano/configuration/null_filter_spec.rb +1 -1
  40. data/spec/lib/capistrano/configuration/question_spec.rb +1 -1
  41. data/spec/lib/capistrano/configuration/role_filter_spec.rb +1 -1
  42. data/spec/lib/capistrano/configuration/server_spec.rb +3 -2
  43. data/spec/lib/capistrano/configuration_spec.rb +16 -3
  44. data/spec/lib/capistrano/doctor/gems_doctor_spec.rb +6 -0
  45. data/spec/lib/capistrano/doctor/servers_doctor_spec.rb +86 -0
  46. data/spec/lib/capistrano/doctor/variables_doctor_spec.rb +9 -0
  47. data/spec/lib/capistrano/dsl/paths_spec.rb +9 -9
  48. data/spec/lib/capistrano/dsl/task_enhancements_spec.rb +5 -0
  49. data/spec/lib/capistrano/dsl_spec.rb +37 -3
  50. data/spec/lib/capistrano/version_validator_spec.rb +2 -2
  51. data/spec/support/test_app.rb +10 -0
  52. metadata +12 -4
@@ -0,0 +1,75 @@
1
+ require "capistrano/proc_helpers"
2
+ require "delegate"
3
+
4
+ module Capistrano
5
+ class Configuration
6
+ # Decorates a Variables object to additionally perform an optional set of
7
+ # user-supplied validation rules. Each rule for a given key is invoked
8
+ # immediately whenever `set` is called with a value for that key.
9
+ #
10
+ # If `set` is called with a block, validation is not performed immediately.
11
+ # Instead, the validation rules are invoked the first time `fetch` is used
12
+ # to access the value.
13
+ #
14
+ # A rule is simply a block that accepts two arguments: key and value. It is
15
+ # up to the rule to raise an exception when it deems the value is invalid
16
+ # (or just print a warning).
17
+ #
18
+ # Rules can be registered using the DSL like this:
19
+ #
20
+ # validate(:my_key) do |key, value|
21
+ # # rule goes here
22
+ # end
23
+ #
24
+ class ValidatedVariables < SimpleDelegator
25
+ include Capistrano::ProcHelpers
26
+
27
+ def initialize(variables)
28
+ super(variables)
29
+ @validators = {}
30
+ end
31
+
32
+ # Decorate Variables#set to add validation behavior.
33
+ def set(key, value=nil, &block)
34
+ if value.nil? && callable_without_parameters?(block)
35
+ super(key, nil, &assert_valid_later(key, &block))
36
+ else
37
+ assert_valid_now(key, block || value)
38
+ super
39
+ end
40
+ end
41
+
42
+ # Register a validation rule for the given key.
43
+ def validate(key, &validator)
44
+ vs = (validators[key] || [])
45
+ vs << validator
46
+ validators[key] = vs
47
+ end
48
+
49
+ private
50
+
51
+ attr_reader :validators
52
+
53
+ # Wrap a block with a proc that validates the value of the block. This
54
+ # allows us to defer validation until the time the value is requested.
55
+ def assert_valid_later(key)
56
+ lambda do
57
+ value = yield
58
+ assert_valid_now(key, value)
59
+ value
60
+ end
61
+ end
62
+
63
+ # Runs all validation rules registered for the given key against the
64
+ # user-supplied value for that variable. If no validator raises an
65
+ # exception, the value is assumed to be valid.
66
+ def assert_valid_now(key, value)
67
+ return unless validators.key?(key)
68
+
69
+ validators[key].each do |validator|
70
+ validator.call(key, value)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -1,9 +1,11 @@
1
+ require "capistrano/proc_helpers"
2
+
1
3
  module Capistrano
2
4
  class Configuration
3
5
  # Holds the variables assigned at Capistrano runtime via `set` and retrieved
4
6
  # with `fetch`. Does internal bookkeeping to help identify user mistakes
5
7
  # like spelling errors or unused variables that may lead to unexpected
6
- # behavior. Also allows validation rules to be registered with `validate`.
8
+ # behavior.
7
9
  class Variables
8
10
  CAPISTRANO_LOCATION = File.expand_path("../..", __FILE__).freeze
9
11
  IGNORED_LOCATIONS = [
@@ -15,6 +17,8 @@ module Capistrano
15
17
  ].freeze
16
18
  private_constant :CAPISTRANO_LOCATION, :IGNORED_LOCATIONS
17
19
 
20
+ include Capistrano::ProcHelpers
21
+
18
22
  def initialize(values={})
19
23
  @trusted_keys = []
20
24
  @fetched_keys = []
@@ -31,7 +35,7 @@ module Capistrano
31
35
  end
32
36
 
33
37
  def set(key, value=nil, &block)
34
- invoke_validations(key, value, &block)
38
+ assert_value_or_block_not_both(value, block)
35
39
  @trusted_keys << key if trusted?
36
40
  remember_location(key)
37
41
  values[key] = block || value
@@ -61,12 +65,6 @@ module Capistrano
61
65
  values.delete(key)
62
66
  end
63
67
 
64
- def validate(key, &validator)
65
- vs = (validators[key] || [])
66
- vs << validator
67
- validators[key] = vs
68
- end
69
-
70
68
  def trusted_keys
71
69
  @trusted_keys.dup
72
70
  end
@@ -106,25 +104,11 @@ module Capistrano
106
104
  (locations[key] ||= []) << location
107
105
  end
108
106
 
109
- def callable_without_parameters?(x)
110
- x.respond_to?(:call) && (!x.respond_to?(:arity) || x.arity == 0)
111
- end
112
-
113
- def validators
114
- @validators ||= {}
115
- end
116
-
117
- def invoke_validations(key, value, &block)
107
+ def assert_value_or_block_not_both(value, block)
118
108
  unless value.nil? || block.nil?
119
109
  raise Capistrano::ValidationError,
120
110
  "Value and block both passed to Configuration#set"
121
111
  end
122
-
123
- return unless validators.key? key
124
-
125
- validators[key].each do |validator|
126
- validator.call(key, block || value)
127
- end
128
112
  end
129
113
 
130
114
  def trace_set(key)
@@ -8,6 +8,16 @@ validate :application do |_key, value|
8
8
  end
9
9
  end
10
10
 
11
+ [:git_strategy, :hg_strategy, :svn_strategy].each do |strategy|
12
+ validate(strategy) do |key, _value|
13
+ warn(
14
+ "[Deprecation Warning] #{key} is deprecated and will be removed in "\
15
+ "Capistrano 3.7.0.\n"\
16
+ "https://github.com/capistrano/capistrano/blob/master/UPGRADING-3.7.md"
17
+ )
18
+ end
19
+ end
20
+
11
21
  set_if_empty :scm, :git
12
22
  set_if_empty :branch, "master"
13
23
  set_if_empty :deploy_to, -> { "/var/www/#{fetch(:application)}" }
@@ -1,5 +1,6 @@
1
1
  require "capistrano/doctor/environment_doctor"
2
2
  require "capistrano/doctor/gems_doctor"
3
3
  require "capistrano/doctor/variables_doctor"
4
+ require "capistrano/doctor/servers_doctor"
4
5
 
5
6
  load File.expand_path("../tasks/doctor.rake", __FILE__)
@@ -34,7 +34,7 @@ module Capistrano
34
34
  end
35
35
 
36
36
  def core_gem_names
37
- %w(capistrano airbrussh rake sshkit) & Gem.loaded_specs.keys
37
+ %w(capistrano airbrussh rake sshkit net-ssh) & Gem.loaded_specs.keys
38
38
  end
39
39
 
40
40
  def plugin_gem_names
@@ -0,0 +1,105 @@
1
+ require "capistrano/doctor/output_helpers"
2
+
3
+ module Capistrano
4
+ module Doctor
5
+ class ServersDoctor
6
+ include Capistrano::Doctor::OutputHelpers
7
+
8
+ def initialize(env=Capistrano::Configuration.env)
9
+ @servers = env.servers.to_a
10
+ end
11
+
12
+ def call
13
+ title("Servers (#{servers.size})")
14
+ rwc = RoleWhitespaceChecker.new(servers)
15
+
16
+ table(servers) do |server, row|
17
+ sd = ServerDecorator.new(server)
18
+
19
+ row << sd.uri_form
20
+ row << sd.roles
21
+ row << sd.properties
22
+ row.yellow if rwc.any_has_whitespace?(server.roles)
23
+ end
24
+
25
+ if rwc.whitespace_roles.any?
26
+ warning "\nWhitespace detected in role(s) #{rwc.whitespace_roles_decorated}. " \
27
+ "This might be a result of a mistyped \"%w()\" array literal."
28
+ end
29
+ puts
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :servers
35
+
36
+ class RoleWhitespaceChecker
37
+ attr_reader :whitespace_roles, :servers
38
+
39
+ def initialize(servers)
40
+ @servers = servers
41
+ @whitespace_roles = find_whitespace_roles
42
+ end
43
+
44
+ def any_has_whitespace?(roles)
45
+ roles.any? { |role| include_whitespace?(role) }
46
+ end
47
+
48
+ def include_whitespace?(role)
49
+ role =~ /\s/
50
+ end
51
+
52
+ def whitespace_roles_decorated
53
+ whitespace_roles.map(&:inspect).join(", ")
54
+ end
55
+
56
+ private
57
+
58
+ def find_whitespace_roles
59
+ servers.map(&:roles).map(&:to_a).flatten.uniq
60
+ .select { |role| include_whitespace?(role) }
61
+ end
62
+ end
63
+
64
+ class ServerDecorator
65
+ def initialize(server)
66
+ @server = server
67
+ end
68
+
69
+ def uri_form
70
+ [
71
+ server.user,
72
+ server.user && "@",
73
+ server.hostname,
74
+ server.port && ":",
75
+ server.port
76
+ ].compact.join
77
+ end
78
+
79
+ def roles
80
+ server.roles.to_a.inspect
81
+ end
82
+
83
+ def properties
84
+ return "" unless server.properties.keys.any?
85
+ pretty_inspect(server.properties.to_h)
86
+ end
87
+
88
+ private
89
+
90
+ attr_reader :server
91
+
92
+ # Hashes with proper padding
93
+ def pretty_inspect(element)
94
+ return element.inspect unless element.is_a?(Hash)
95
+
96
+ pairs_string = element.keys.map do |key|
97
+ [pretty_inspect(key), pretty_inspect(element.fetch(key))].join(" => ")
98
+ end.join(", ")
99
+
100
+ "{ #{pairs_string} }"
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -7,7 +7,7 @@ module Capistrano
7
7
  class VariablesDoctor
8
8
  # These are keys that have no default values in Capistrano, but are
9
9
  # nonetheless expected to be set.
10
- WHITELIST = [:application, :repo_url].freeze
10
+ WHITELIST = [:application, :repo_url, :git_strategy, :hg_strategy, :svn_strategy].freeze
11
11
  private_constant :WHITELIST
12
12
 
13
13
  include Capistrano::Doctor::OutputHelpers
@@ -20,18 +20,17 @@ module Capistrano
20
20
  title("Variables")
21
21
  values = inspect_all_values
22
22
 
23
- table(variables.keys.sort) do |key, row|
23
+ table(variables.keys.sort_by(&:to_s)) do |key, row|
24
24
  row.yellow if suspicious_keys.include?(key)
25
- row << ":#{key}"
25
+ row << key.inspect
26
26
  row << values[key]
27
27
  end
28
28
 
29
29
  puts if suspicious_keys.any?
30
30
 
31
- suspicious_keys.sort.each do |key|
32
- warning(
33
- ":#{key} is not a recognized Capistrano setting (#{location(key)})"
34
- )
31
+ suspicious_keys.sort_by(&:to_s).each do |key|
32
+ warning("#{key.inspect} is not a recognized Capistrano setting "\
33
+ "(#{location(key)})")
35
34
  end
36
35
  end
37
36
 
@@ -11,8 +11,18 @@ module Capistrano
11
11
  include Paths
12
12
  include Stages
13
13
 
14
- def invoke(task, *args)
15
- Rake::Task[task].invoke(*args)
14
+ def invoke(task_name, *args)
15
+ task = Rake::Task[task_name]
16
+ if task && task.already_invoked
17
+ file, line, = caller.first.split(":")
18
+ colors = SSHKit::Color.new($stderr)
19
+ $stderr.puts colors.colorize("Skipping task `#{task_name}'.", :yellow)
20
+ $stderr.puts "Capistrano tasks may only be invoked once. Since task `#{task}' was previously invoked, invoke(\"#{task_name}\") at #{file}:#{line} will be skipped."
21
+ $stderr.puts "If you really meant to run this task again, first call Rake::Task[\"#{task_name}\"].reenable"
22
+ $stderr.puts colors.colorize("THIS BEHAVIOR MAY CHANGE IN A FUTURE VERSION OF CAPISTRANO. Please join the conversation here if this affects you.", :red)
23
+ $stderr.puts colors.colorize("https://github.com/capistrano/capistrano/issues/1686", :red)
24
+ end
25
+ task.invoke(*args)
16
26
  end
17
27
 
18
28
  def t(key, options={})
@@ -33,8 +43,7 @@ module Capistrano
33
43
  branch: fetch(:branch),
34
44
  user: local_user,
35
45
  sha: fetch(:current_revision),
36
- release: fetch(:release_timestamp))
37
- )
46
+ release: fetch(:release_timestamp)))
38
47
  end
39
48
 
40
49
  def rollback_log_message
@@ -57,6 +66,21 @@ module Capistrano
57
66
  def run_locally(&block)
58
67
  SSHKit::Backend::Local.new(&block).run
59
68
  end
69
+
70
+ # Catch common beginner mistake and give a helpful error message on stderr
71
+ def execute(*)
72
+ file, line, = caller.first.split(":")
73
+ colors = SSHKit::Color.new($stderr)
74
+ $stderr.puts colors.colorize("Warning: `execute' should be wrapped in an `on' scope in #{file}:#{line}.", :red)
75
+ $stderr.puts
76
+ $stderr.puts " task :example do"
77
+ $stderr.puts colors.colorize(" on roles(:app) do", :yellow)
78
+ $stderr.puts " execute 'whoami'"
79
+ $stderr.puts colors.colorize(" end", :yellow)
80
+ $stderr.puts " end"
81
+ $stderr.puts
82
+ raise NoMethodError, "undefined method `execute' for main:Object"
83
+ end
60
84
  end
61
85
  end
62
86
  extend Capistrano::DSL
@@ -11,7 +11,7 @@ module Capistrano
11
11
  end
12
12
 
13
13
  def current_path
14
- deploy_path.join("current")
14
+ deploy_path.join(fetch(:current_directory, "current"))
15
15
  end
16
16
 
17
17
  def releases_path
@@ -1,8 +1,13 @@
1
1
  module Capistrano
2
2
  module DSL
3
3
  module Stages
4
+ RESERVED_NAMES = %w(deploy doctor install).freeze
5
+ private_constant :RESERVED_NAMES
6
+
4
7
  def stages
5
- Dir[stage_definitions].map { |f| File.basename(f, ".rb") }
8
+ names = Dir[stage_definitions].map { |f| File.basename(f, ".rb") }
9
+ assert_valid_stage_names(names)
10
+ names
6
11
  end
7
12
 
8
13
  def stage_definitions
@@ -12,6 +17,15 @@ module Capistrano
12
17
  def stage_set?
13
18
  !!fetch(:stage, false)
14
19
  end
20
+
21
+ private
22
+
23
+ def assert_valid_stage_names(names)
24
+ invalid = names.find { |n| RESERVED_NAMES.include?(n) }
25
+ return if invalid.nil?
26
+
27
+ raise t("error.invalid_stage_name", name: invalid, path: stage_config_path.join("#{invalid}.rb"))
28
+ end
15
29
  end
16
30
  end
17
31
  end
@@ -11,11 +11,16 @@ module Capistrano
11
11
  Rake::Task.define_task(post_task, *args, &block) if block_given?
12
12
  task = Rake::Task[task]
13
13
  task.enhance do
14
- Rake.application.lookup(post_task, task.scope).invoke
14
+ post = Rake.application.lookup(post_task, task.scope)
15
+ raise ArgumentError, "Task #{post_task.inspect} not found" unless post
16
+ post.invoke
15
17
  end
16
18
  end
17
19
 
18
20
  def remote_file(task)
21
+ warn("[Deprecation Warning] `remote_file` is deprecated and will be "\
22
+ "removed in Capistrano 3.7.0")
23
+
19
24
  target_roles = task.delete(:roles) { :all }
20
25
  define_remote_file_task(task, target_roles)
21
26
  end
@@ -15,6 +15,7 @@ en = {
15
15
  no_old_releases: "No old releases (keeping newest %{keep_releases}) on %{host}",
16
16
  linked_file_does_not_exist: "linked file %{file} does not exist on %{host}",
17
17
  cannot_rollback: "There are no older releases to rollback to",
18
+ cannot_found_rollback_release: "Cannot rollback because release %{release} does not exist",
18
19
  mirror_exists: "The repository mirror is at %{at}",
19
20
  revision_log_message: "Branch %{branch} (at %{sha}) deployed as release %{release} by %{user}",
20
21
  rollback_log_message: "%{user} rolled back to release %{release}",
@@ -24,6 +25,7 @@ en = {
24
25
  bye: "bye"
25
26
  },
26
27
  error: {
28
+ invalid_stage_name: '"%{name}" is a reserved word and cannot be used as a stage. Rename "%{path}" to something else.',
27
29
  user: {
28
30
  does_not_exist: "User %{user} does not exists",
29
31
  cannot_switch: "Cannot switch to user %{user}"