Skip to content

Commit

Permalink
Add doctor:variables, :environment, and :gems
Browse files Browse the repository at this point in the history
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
mattbrictson authored and leehambley committed Apr 25, 2016
1 parent 66f7bae commit b5e4aa6
Show file tree
Hide file tree
Showing 17 changed files with 640 additions and 77 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
10 changes: 8 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions features/doctor.feature
Original file line number Diff line number Diff line change
@@ -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
11 changes: 4 additions & 7 deletions issue_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`):
77 changes: 11 additions & 66 deletions lib/capistrano/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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?)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
136 changes: 136 additions & 0 deletions lib/capistrano/configuration/variables.rb
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions lib/capistrano/doctor.rb
Original file line number Diff line number Diff line change
@@ -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__)
19 changes: 19 additions & 0 deletions lib/capistrano/doctor/environment_doctor.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit b5e4aa6

Please sign in to comment.