capistrano 3.5.0 → 3.6.0

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