Permalink
Browse files

Add doctor:variables, :environment, and :gems

This adds various "doctor" tasks that can be used for troubleshooting. To see
all the doctor output, run e.g. `cap production doctor`. This will print a
report like this:

```
Environment

    Ruby     ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-darwin14]
    Rubygems 2.6.2
    Bundler  1.11.2
    Command  cap production doctor

Gems

    capistrano         3.4.0
    airbrussh          1.0.1
    rake               10.5.0 (update available)
    sshkit             1.9.0
    capistrano-bundler 1.1.4
    capistrano-rails   1.1.6

Variables

    :application                         "myapp"
    :assets_prefix                       "assets"
    :assets_roles                        [:web]
    :branch                              "master"

... etc.
```

To obtain the variables information in particular, code has been added to audit
the setting and fetching of variables. Variables set by Capistrano itself and
its plugins are whitelisted, but others are "untrusted". If a variable is
untrusted and it seems like it is never used, then `doctor:variables` will print
a warning (include source location) for that variable name, like this:

```
:copy_strategy is not a recognized Capistrano setting (config/deploy.rb:14)
```

Finally, the RubyGems API is used to check the remote gem repository to see if
any newer versions of Capistrano gems are available (this is gracefully skipped
if there is no network connection). Any outdated gems will be indicated in the
`doctor:gems` output.
  • Loading branch information...
1 parent 66f7bae commit b5e4aa6d5e01699960270a9b20bc4b62709cbfb7 @mattbrictson mattbrictson committed with leehambley Mar 10, 2016
View
@@ -31,6 +31,7 @@ and how to configure it, visit the
### New features:
+* Added a `doctor` task that outputs helpful troubleshooting information. Try it like this: `cap production doctor`. (@mattbrictson)
* Added a `dry_run?` helper method
* `remove` DSL method for removing values like from arrays like `linked_dirs`
* `append` DSL method for pushing values like `linked_dirs`
View
@@ -25,9 +25,15 @@ As much the Capistrano community tries to write good, well-tested code, bugs sti
**In case you’ve run across an already-known issue, check the FAQs first on the [official Capistrano site](http://capistranorb.com).**
-When opening a bug report, please include the following:
+When opening a bug report, please include the output of the `cap <stage> doctor` task, e.g.:
-* Versions of Ruby, Capistrano, and any plugins you’re using
+```
+cap production doctor
+```
+
+Also include in your report:
+
+* Versions of Ruby, Capistrano, and any plugins you’re using (if `doctor` didn't already do this for you)
* A description of the troubleshooting steps you’ve taken
* Logs and backtraces
* Sections of your `deploy.rb` that may be relevant
@@ -0,0 +1,11 @@
+Feature: Doctor
+
+ Background:
+ Given a test app with the default configuration
+
+ Scenario: Running the doctor task
+ When I run cap "doctor"
+ Then the task is successful
+ And contains "Environment" in the output
+ And contains "Gems" in the output
+ And contains "Variables" in the output
View
@@ -3,22 +3,19 @@
---
#### Steps to reproduce
+
1. Lorem.
2. Ipsum..
3. Dolor...
#### Expected behaviour
+
Tell us what should happen
#### Actual behaviour
+
Tell us what happens instead
#### Your configuration
-**Your Operating system (`$ uname -a` if on Linux/Mac)**:
-
-**Your Ruby Version (`$ ruby -v`):**
-
-**Your Capistrano version (`$ cap --version`):**
-
-**Your Capistrano Plugins (`$ bundle list | grep capistrano-`): **
+Paste Capistrano's `doctor` output here (`cap <stage> doctor`):
@@ -3,15 +3,12 @@
require_relative "configuration/plugin_installer"
require_relative "configuration/server"
require_relative "configuration/servers"
+require_relative "configuration/variables"
module Capistrano
class ValidationError < Exception; end
class Configuration
- def initialize(config=nil)
- @config ||= config
- end
-
def self.env
@env ||= new
end
@@ -20,22 +17,22 @@ def self.reset!
@env = new
end
+ extend Forwardable
+ attr_reader :variables
+ def_delegators :variables,
+ :set, :fetch, :fetch_for, :delete, :keys, :validate
+
+ def initialize(values={})
+ @variables = Variables.new(values)
+ end
+
def ask(key, default=nil, options={})
question = Question.new(key, default, options)
set(key, question)
end
- def set(key, value=nil, &block)
- invoke_validations(key, value, &block)
- config[key] = block || value
-
- puts "Config variable set: #{key.inspect} => #{config[key].inspect}" if fetch(:print_config_variables, false)
-
- config[key]
- end
-
def set_if_empty(key, value=nil, &block)
- set(key, value, &block) unless config.key? key
+ set(key, value, &block) unless keys.include?(key)
end
def append(key, *values)
@@ -46,16 +43,6 @@ def remove(key, *values)
set(key, Array(fetch(key)) - values)
end
- def delete(key)
- config.delete(key)
- end
-
- def fetch(key, default=nil, &block)
- value = fetch_for(key, default, &block)
- value = set(key, value.call) while callable_without_parameters?(value)
- value
- end
-
def any?(key)
value = fetch(key)
if value && value.respond_to?(:any?)
@@ -65,16 +52,6 @@ def any?(key)
end
end
- def validate(key, &validator)
- vs = (validators[key] || [])
- vs << validator
- validators[key] = vs
- end
-
- def keys
- config.keys
- end
-
def is_question?(key)
value = fetch_for(key, nil)
!value.nil? && value.is_a?(Question)
@@ -166,42 +143,10 @@ def servers
@servers ||= Servers.new
end
- def config
- @config ||= {}
- end
-
- def validators
- @validators ||= {}
- end
-
def installer
@installer ||= PluginInstaller.new
end
- def fetch_for(key, default, &block)
- if block_given?
- config.fetch(key, &block)
- else
- config.fetch(key, default)
- end
- end
-
- def callable_without_parameters?(x)
- x.respond_to?(:call) && (!x.respond_to?(:arity) || x.arity == 0)
- end
-
- def invoke_validations(key, value, &block)
- unless value.nil? || block.nil?
- raise Capistrano::ValidationError, "Value and block both passed to Configuration#set"
- end
-
- return unless validators.key? key
-
- validators[key].each do |validator|
- validator.call(key, block || value)
- end
- end
-
def configure_sshkit_output(sshkit)
format_args = [fetch(:format)]
format_args.push(fetch(:format_options)) if any?(:format_options)
@@ -0,0 +1,136 @@
+module Capistrano
+ class Configuration
+ # Holds the variables assigned at Capistrano runtime via `set` and retrieved
+ # with `fetch`. Does internal bookkeeping to help identify user mistakes
+ # like spelling errors or unused variables that may lead to unexpected
+ # behavior. Also allows validation rules to be registered with `validate`.
+ class Variables
+ CAPISTRANO_LOCATION = File.expand_path("../..", __FILE__).freeze
+ IGNORED_LOCATIONS = [
+ "#{CAPISTRANO_LOCATION}/configuration/variables.rb:",
+ "#{CAPISTRANO_LOCATION}/configuration.rb:",
+ "#{CAPISTRANO_LOCATION}/dsl/env.rb:",
+ "/dsl.rb:",
+ "/forwardable.rb:"
+ ].freeze
+ private_constant :CAPISTRANO_LOCATION, :IGNORED_LOCATIONS
+
+ def initialize(values={})
+ @trusted_keys = []
+ @fetched_keys = []
+ @locations = {}
+ @values = values
+ @trusted = true
+ end
+
+ def untrusted!
+ @trusted = false
+ yield
+ ensure
+ @trusted = true
+ end
+
+ def set(key, value=nil, &block)
+ invoke_validations(key, value, &block)
+ @trusted_keys << key if trusted?
+ remember_location(key)
+ values[key] = block || value
+ trace_set(key)
+ values[key]
+ end
+
+ def fetch(key, default=nil, &block)
+ fetched_keys << key
+ peek(key, default, &block)
+ end
+
+ # Internal use only.
+ def peek(key, default=nil, &block)
+ value = fetch_for(key, default, &block)
+ while callable_without_parameters?(value)
+ value = (values[key] = value.call)
+ end
+ value
+ end
+
+ def fetch_for(key, default, &block)
+ block ? values.fetch(key, &block) : values.fetch(key, default)
+ end
+
+ def delete(key)
+ values.delete(key)
+ end
+
+ def validate(key, &validator)
+ vs = (validators[key] || [])
+ vs << validator
+ validators[key] = vs
+ end
+
+ def trusted_keys
+ @trusted_keys.dup
+ end
+
+ def untrusted_keys
+ keys - @trusted_keys
+ end
+
+ def keys
+ values.keys
+ end
+
+ # Keys that have been set, but which have never been fetched.
+ def unused_keys
+ keys - fetched_keys
+ end
+
+ # Returns an array of source file location(s) where the given key was
+ # assigned (i.e. where `set` was called). If the key was never assigned,
+ # returns `nil`.
+ def source_locations(key)
+ locations[key]
+ end
+
+ private
+
+ attr_reader :locations, :values, :fetched_keys
+
+ def trusted?
+ @trusted
+ end
+
+ def remember_location(key)
+ location = caller.find do |line|
+ IGNORED_LOCATIONS.none? { |i| line.include?(i) }
+ end
+ (locations[key] ||= []) << location
+ end
+
+ def callable_without_parameters?(x)
+ x.respond_to?(:call) && (!x.respond_to?(:arity) || x.arity == 0)
+ end
+
+ def validators
+ @validators ||= {}
+ end
+
+ def invoke_validations(key, value, &block)
+ unless value.nil? || block.nil?
+ raise Capistrano::ValidationError,
+ "Value and block both passed to Configuration#set"
+ end
+
+ return unless validators.key? key
+
+ validators[key].each do |validator|
+ validator.call(key, block || value)
+ end
+ end
+
+ def trace_set(key)
+ return unless fetch(:print_config_variables, false)
+ puts "Config variable set: #{key.inspect} => #{values[key].inspect}"
+ end
+ end
+ end
+end
@@ -0,0 +1,5 @@
+require "capistrano/doctor/environment_doctor"
+require "capistrano/doctor/gems_doctor"
+require "capistrano/doctor/variables_doctor"
+
+load File.expand_path("../tasks/doctor.rake", __FILE__)
@@ -0,0 +1,19 @@
+require "capistrano/doctor/output_helpers"
+
+module Capistrano
+ module Doctor
+ class EnvironmentDoctor
+ include Capistrano::Doctor::OutputHelpers
+
+ def call
+ title("Environment")
+ puts <<-OUT.gsub(/^\s+/, "")
+ Ruby #{RUBY_DESCRIPTION}
+ Rubygems #{Gem::VERSION}
+ Bundler #{defined?(Bundler::VERSION) ? Bundler::VERSION : 'N/A'}
+ Command #{$PROGRAM_NAME} #{ARGV.join(' ')}
+ OUT
+ end
+ end
+ end
+end
Oops, something went wrong.

0 comments on commit b5e4aa6

Please sign in to comment.