capistrano 2.0.0 → 2.15.2

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 (125) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.travis.yml +7 -0
  4. data/CHANGELOG +715 -18
  5. data/Gemfile +12 -0
  6. data/README.md +94 -0
  7. data/Rakefile +11 -0
  8. data/bin/cap +0 -0
  9. data/bin/capify +37 -22
  10. data/capistrano.gemspec +40 -0
  11. data/lib/capistrano/callback.rb +5 -1
  12. data/lib/capistrano/cli/execute.rb +10 -7
  13. data/lib/capistrano/cli/help.rb +39 -16
  14. data/lib/capistrano/cli/help.txt +44 -16
  15. data/lib/capistrano/cli/options.rb +71 -11
  16. data/lib/capistrano/cli/ui.rb +13 -1
  17. data/lib/capistrano/cli.rb +5 -5
  18. data/lib/capistrano/command.rb +215 -58
  19. data/lib/capistrano/configuration/actions/file_transfer.rb +29 -14
  20. data/lib/capistrano/configuration/actions/inspect.rb +3 -3
  21. data/lib/capistrano/configuration/actions/invocation.rb +212 -22
  22. data/lib/capistrano/configuration/alias_task.rb +26 -0
  23. data/lib/capistrano/configuration/callbacks.rb +26 -27
  24. data/lib/capistrano/configuration/connections.rb +130 -52
  25. data/lib/capistrano/configuration/execution.rb +34 -18
  26. data/lib/capistrano/configuration/loading.rb +91 -6
  27. data/lib/capistrano/configuration/log_formatters.rb +75 -0
  28. data/lib/capistrano/configuration/namespaces.rb +45 -12
  29. data/lib/capistrano/configuration/roles.rb +28 -2
  30. data/lib/capistrano/configuration/servers.rb +51 -10
  31. data/lib/capistrano/configuration/variables.rb +3 -3
  32. data/lib/capistrano/configuration.rb +20 -4
  33. data/lib/capistrano/errors.rb +12 -8
  34. data/lib/capistrano/ext/multistage.rb +62 -0
  35. data/lib/capistrano/ext/string.rb +5 -0
  36. data/lib/capistrano/extensions.rb +1 -1
  37. data/lib/capistrano/fix_rake_deprecated_dsl.rb +8 -0
  38. data/lib/capistrano/logger.rb +112 -5
  39. data/lib/capistrano/processable.rb +55 -0
  40. data/lib/capistrano/recipes/compat.rb +2 -2
  41. data/lib/capistrano/recipes/deploy/assets.rb +185 -0
  42. data/lib/capistrano/recipes/deploy/dependencies.rb +2 -2
  43. data/lib/capistrano/recipes/deploy/local_dependency.rb +10 -2
  44. data/lib/capistrano/recipes/deploy/remote_dependency.rb +54 -2
  45. data/lib/capistrano/recipes/deploy/scm/accurev.rb +169 -0
  46. data/lib/capistrano/recipes/deploy/scm/base.rb +31 -11
  47. data/lib/capistrano/recipes/deploy/scm/bzr.rb +14 -14
  48. data/lib/capistrano/recipes/deploy/scm/cvs.rb +10 -8
  49. data/lib/capistrano/recipes/deploy/scm/darcs.rb +12 -1
  50. data/lib/capistrano/recipes/deploy/scm/git.rb +293 -0
  51. data/lib/capistrano/recipes/deploy/scm/mercurial.rb +23 -15
  52. data/lib/capistrano/recipes/deploy/scm/none.rb +55 -0
  53. data/lib/capistrano/recipes/deploy/scm/perforce.rb +54 -28
  54. data/lib/capistrano/recipes/deploy/scm/subversion.rb +35 -17
  55. data/lib/capistrano/recipes/deploy/scm.rb +1 -1
  56. data/lib/capistrano/recipes/deploy/strategy/base.rb +32 -4
  57. data/lib/capistrano/recipes/deploy/strategy/copy.rb +238 -43
  58. data/lib/capistrano/recipes/deploy/strategy/remote.rb +1 -1
  59. data/lib/capistrano/recipes/deploy/strategy/remote_cache.rb +11 -1
  60. data/lib/capistrano/recipes/deploy/strategy/unshared_remote_cache.rb +21 -0
  61. data/lib/capistrano/recipes/deploy/strategy.rb +1 -1
  62. data/lib/capistrano/recipes/deploy.rb +265 -123
  63. data/lib/capistrano/recipes/standard.rb +1 -1
  64. data/lib/capistrano/role.rb +102 -0
  65. data/lib/capistrano/server_definition.rb +6 -1
  66. data/lib/capistrano/shell.rb +30 -33
  67. data/lib/capistrano/ssh.rb +46 -60
  68. data/lib/capistrano/task_definition.rb +16 -8
  69. data/lib/capistrano/transfer.rb +218 -0
  70. data/lib/capistrano/version.rb +6 -17
  71. data/lib/capistrano.rb +4 -1
  72. data/test/cli/execute_test.rb +3 -3
  73. data/test/cli/help_test.rb +33 -7
  74. data/test/cli/options_test.rb +109 -6
  75. data/test/cli/ui_test.rb +2 -2
  76. data/test/cli_test.rb +3 -3
  77. data/test/command_test.rb +144 -124
  78. data/test/configuration/actions/file_transfer_test.rb +41 -20
  79. data/test/configuration/actions/inspect_test.rb +21 -7
  80. data/test/configuration/actions/invocation_test.rb +91 -30
  81. data/test/configuration/alias_task_test.rb +118 -0
  82. data/test/configuration/callbacks_test.rb +41 -46
  83. data/test/configuration/connections_test.rb +187 -36
  84. data/test/configuration/execution_test.rb +18 -2
  85. data/test/configuration/loading_test.rb +17 -4
  86. data/test/configuration/namespace_dsl_test.rb +54 -5
  87. data/test/configuration/roles_test.rb +114 -4
  88. data/test/configuration/servers_test.rb +97 -4
  89. data/test/configuration/variables_test.rb +12 -2
  90. data/test/configuration_test.rb +9 -13
  91. data/test/deploy/local_dependency_test.rb +76 -0
  92. data/test/deploy/remote_dependency_test.rb +146 -0
  93. data/test/deploy/scm/accurev_test.rb +23 -0
  94. data/test/deploy/scm/base_test.rb +1 -1
  95. data/test/deploy/scm/bzr_test.rb +51 -0
  96. data/test/deploy/scm/darcs_test.rb +37 -0
  97. data/test/deploy/scm/git_test.rb +221 -0
  98. data/test/deploy/scm/mercurial_test.rb +134 -0
  99. data/test/deploy/scm/none_test.rb +35 -0
  100. data/test/deploy/scm/perforce_test.rb +23 -0
  101. data/test/deploy/scm/subversion_test.rb +40 -0
  102. data/test/deploy/strategy/copy_test.rb +240 -26
  103. data/test/extensions_test.rb +2 -2
  104. data/test/logger_formatting_test.rb +149 -0
  105. data/test/logger_test.rb +13 -2
  106. data/test/recipes_test.rb +25 -0
  107. data/test/role_test.rb +11 -0
  108. data/test/server_definition_test.rb +15 -2
  109. data/test/shell_test.rb +33 -1
  110. data/test/ssh_test.rb +40 -24
  111. data/test/task_definition_test.rb +18 -2
  112. data/test/transfer_test.rb +168 -0
  113. data/test/utils.rb +27 -33
  114. metadata +215 -102
  115. data/MIT-LICENSE +0 -20
  116. data/README +0 -43
  117. data/examples/sample.rb +0 -14
  118. data/lib/capistrano/gateway.rb +0 -131
  119. data/lib/capistrano/recipes/deploy/templates/maintenance.rhtml +0 -53
  120. data/lib/capistrano/recipes/templates/maintenance.rhtml +0 -53
  121. data/lib/capistrano/recipes/upgrade.rb +0 -33
  122. data/lib/capistrano/upload.rb +0 -146
  123. data/test/gateway_test.rb +0 -167
  124. data/test/upload_test.rb +0 -131
  125. data/test/version_test.rb +0 -24
@@ -1,15 +1,19 @@
1
1
  module Capistrano
2
- class Error < RuntimeError; end
3
2
 
4
- class CaptureError < Error; end
5
- class NoSuchTaskError < Error; end
6
- class NoMatchingServersError < Error; end
7
-
3
+ Error = Class.new(RuntimeError)
4
+
5
+ CaptureError = Class.new(Capistrano::Error)
6
+ NoSuchTaskError = Class.new(Capistrano::Error)
7
+ NoMatchingServersError = Class.new(Capistrano::Error)
8
+
8
9
  class RemoteError < Error
9
10
  attr_accessor :hosts
10
11
  end
11
12
 
12
- class ConnectionError < RemoteError; end
13
- class UploadError < RemoteError; end
14
- class CommandError < RemoteError; end
13
+ ConnectionError = Class.new(Capistrano::RemoteError)
14
+ TransferError = Class.new(Capistrano::RemoteError)
15
+ CommandError = Class.new(Capistrano::RemoteError)
16
+
17
+ LocalArgumentError = Class.new(Capistrano::Error)
18
+
15
19
  end
@@ -0,0 +1,62 @@
1
+ require 'fileutils'
2
+
3
+ unless Capistrano::Configuration.respond_to?(:instance)
4
+ abort "capistrano/ext/multistage requires Capistrano 2"
5
+ end
6
+
7
+ Capistrano::Configuration.instance.load do
8
+ location = fetch(:stage_dir, "config/deploy")
9
+
10
+ unless exists?(:stages)
11
+ set :stages, Dir["#{location}/*.rb"].map { |f| File.basename(f, ".rb") }
12
+ end
13
+
14
+ stages.each do |name|
15
+ desc "Set the target stage to `#{name}'."
16
+ task(name) do
17
+ set :stage, name.to_sym
18
+ load "#{location}/#{stage}" if File.exist?(File.join(location, "#{stage}.rb"))
19
+ end
20
+ end
21
+
22
+ on :load do
23
+ if stages.include?(ARGV.first)
24
+ # Execute the specified stage so that recipes required in stage can contribute to task list
25
+ find_and_execute_task(ARGV.first) if ARGV.any?{ |option| option =~ /-T|--tasks|-e|--explain/ }
26
+ else
27
+ # Execute the default stage so that recipes required in stage can contribute tasks
28
+ find_and_execute_task(default_stage) if exists?(:default_stage)
29
+ end
30
+ end
31
+
32
+ namespace :multistage do
33
+ desc "[internal] Ensure that a stage has been selected."
34
+ task :ensure do
35
+ if !exists?(:stage)
36
+ if exists?(:default_stage)
37
+ logger.important "Defaulting to `#{default_stage}'"
38
+ find_and_execute_task(default_stage)
39
+ else
40
+ abort "No stage specified. Please specify one of: #{stages.join(', ')} (e.g. `cap #{stages.first} #{ARGV.last}')"
41
+ end
42
+ end
43
+ end
44
+
45
+ desc "Stub out the staging config files."
46
+ task :prepare do
47
+ FileUtils.mkdir_p(location)
48
+ stages.each do |name|
49
+ file = File.join(location, name + ".rb")
50
+ unless File.exists?(file)
51
+ File.open(file, "w") do |f|
52
+ f.puts "# #{name.upcase}-specific deployment configuration"
53
+ f.puts "# please put general deployment config in config/deploy.rb"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ on :start, "multistage:ensure", :except => stages + ['multistage:prepare']
61
+
62
+ end
@@ -0,0 +1,5 @@
1
+ class String
2
+ def compact
3
+ self.gsub(/\s+/, ' ')
4
+ end
5
+ end
@@ -25,7 +25,7 @@ module Capistrano
25
25
  Capistrano::Configuration.protected_instance_methods +
26
26
  Capistrano::Configuration.private_instance_methods
27
27
 
28
- if methods.include?(name.to_s)
28
+ if methods.any? { |m| m.to_sym == name }
29
29
  raise Capistrano::Error, "registering a plugin named `#{name}' would shadow a method on Capistrano::Configuration with the same name"
30
30
  end
31
31
 
@@ -0,0 +1,8 @@
1
+ #
2
+ # See https://github.com/jimweirich/rake/issues/81
3
+ #
4
+ if defined?(Rake::DeprecatedObjectDSL)
5
+ Rake::DeprecatedObjectDSL.private_instance_methods.each do |m|
6
+ Rake::DeprecatedObjectDSL.send("undef_method", m)
7
+ end
8
+ end
@@ -1,15 +1,80 @@
1
1
  module Capistrano
2
2
  class Logger #:nodoc:
3
- attr_accessor :level
4
- attr_reader :device
3
+ attr_accessor :level, :device, :disable_formatters
5
4
 
6
5
  IMPORTANT = 0
7
6
  INFO = 1
8
7
  DEBUG = 2
9
8
  TRACE = 3
10
-
9
+
11
10
  MAX_LEVEL = 3
12
11
 
12
+ COLORS = {
13
+ :none => "0",
14
+ :black => "30",
15
+ :red => "31",
16
+ :green => "32",
17
+ :yellow => "33",
18
+ :blue => "34",
19
+ :magenta => "35",
20
+ :cyan => "36",
21
+ :white => "37"
22
+ }
23
+
24
+ STYLES = {
25
+ :bright => 1,
26
+ :dim => 2,
27
+ :underscore => 4,
28
+ :blink => 5,
29
+ :reverse => 7,
30
+ :hidden => 8
31
+ }
32
+
33
+ # Set up default formatters
34
+ @default_formatters = [
35
+ # TRACE
36
+ { :match => /command finished/, :color => :white, :style => :dim, :level => 3, :priority => -10 },
37
+ { :match => /executing locally/, :color => :yellow, :level => 3, :priority => -20 },
38
+
39
+ # DEBUG
40
+ { :match => /executing `.*/, :color => :green, :level => 2, :priority => -10, :timestamp => true },
41
+ { :match => /.*/, :color => :yellow, :level => 2, :priority => -30 },
42
+
43
+ # INFO
44
+ { :match => /.*out\] (fatal:|ERROR:).*/, :color => :red, :level => 1, :priority => -10 },
45
+ { :match => /Permission denied/, :color => :red, :level => 1, :priority => -20 },
46
+ { :match => /sh: .+: command not found/, :color => :magenta, :level => 1, :priority => -30 },
47
+
48
+ # IMPORTANT
49
+ { :match => /^err ::/, :color => :red, :level => 0, :priority => -10 },
50
+ { :match => /.*/, :color => :blue, :level => 0, :priority => -20 }
51
+ ]
52
+ @formatters = @default_formatters
53
+
54
+ class << self
55
+ def default_formatters
56
+ @default_formatters
57
+ end
58
+
59
+ def default_formatters=(defaults=nil)
60
+ @default_formatters = [defaults].flatten
61
+
62
+ # reset the formatters
63
+ @formatters = @default_formatters
64
+ @sorted_formatters = nil
65
+ end
66
+
67
+ def add_formatter(options) #:nodoc:
68
+ @formatters.push(options)
69
+ @sorted_formatters = nil
70
+ end
71
+
72
+ def sorted_formatters
73
+ # Sort matchers in reverse order so we can break if we found a match.
74
+ @sorted_formatters ||= @formatters.sort_by { |i| -(i[:priority] || i[:prio] || 0) }
75
+ end
76
+ end
77
+
13
78
  def initialize(options={})
14
79
  output = options[:output] || $stderr
15
80
  if output.respond_to?(:puts)
@@ -20,7 +85,8 @@ module Capistrano
20
85
  end
21
86
 
22
87
  @options = options
23
- @level = 0
88
+ @level = options[:level] || 0
89
+ @disable_formatters = options[:disable_formatters]
24
90
  end
25
91
 
26
92
  def close
@@ -29,8 +95,44 @@ module Capistrano
29
95
 
30
96
  def log(level, message, line_prefix=nil)
31
97
  if level <= self.level
98
+ # Only format output if device is a TTY or formatters are not disabled
99
+ if device.tty? && !@disable_formatters
100
+ color = :none
101
+ style = nil
102
+
103
+ Logger.sorted_formatters.each do |formatter|
104
+ if (formatter[:level] == level || formatter[:level].nil?)
105
+ if message =~ formatter[:match] || line_prefix =~ formatter[:match]
106
+ color = formatter[:color] if formatter[:color]
107
+ style = formatter[:style] || formatter[:attribute] # (support original cap colors)
108
+ message.gsub!(formatter[:match], formatter[:replace]) if formatter[:replace]
109
+ message = formatter[:prepend] + message unless formatter[:prepend].nil?
110
+ message = message + formatter[:append] unless formatter[:append].nil?
111
+ message = Time.now.strftime('%Y-%m-%d %T') + ' ' + message if formatter[:timestamp]
112
+ break unless formatter[:replace]
113
+ end
114
+ end
115
+ end
116
+
117
+ if color == :hide
118
+ # Don't do anything if color is set to :hide
119
+ return false
120
+ end
121
+
122
+ term_color = COLORS[color]
123
+ term_style = STYLES[style]
124
+
125
+ # Don't format message if no color or style
126
+ unless color == :none and style.nil?
127
+ unless line_prefix.nil?
128
+ line_prefix = format(line_prefix, term_color, term_style, nil)
129
+ end
130
+ message = format(message, term_color, term_style)
131
+ end
132
+ end
133
+
32
134
  indent = "%*s" % [MAX_LEVEL, "*" * (MAX_LEVEL - level)]
33
- message.each do |line|
135
+ (RUBY_VERSION >= "1.9" ? message.lines : message).each do |line|
34
136
  if line_prefix
35
137
  device.puts "#{indent} [#{line_prefix}] #{line.strip}\n"
36
138
  else
@@ -55,5 +157,10 @@ module Capistrano
55
157
  def trace(message, line_prefix=nil)
56
158
  log(TRACE, message, line_prefix)
57
159
  end
160
+
161
+ def format(message, color, style, nl = "\n")
162
+ style = "#{style};" if style
163
+ "\e[#{style}#{color}m" + message.to_s.strip + "\e[0m#{nl}"
164
+ end
58
165
  end
59
166
  end
@@ -0,0 +1,55 @@
1
+ module Capistrano
2
+ module Processable
3
+ module SessionAssociation
4
+ def self.on(exception, session)
5
+ unless exception.respond_to?(:session)
6
+ exception.extend(self)
7
+ exception.session = session
8
+ end
9
+
10
+ return exception
11
+ end
12
+
13
+ attr_accessor :session
14
+ end
15
+
16
+ def process_iteration(wait=nil, &block)
17
+ ensure_each_session { |session| session.preprocess }
18
+
19
+ return false if block && !block.call(self)
20
+
21
+ readers = sessions.map { |session| session.listeners.keys }.flatten.reject { |io| io.closed? }
22
+ writers = readers.select { |io| io.respond_to?(:pending_write?) && io.pending_write? }
23
+
24
+ if readers.any? || writers.any?
25
+ readers, writers, = IO.select(readers, writers, nil, wait)
26
+ else
27
+ return false
28
+ end
29
+
30
+ if readers
31
+ ensure_each_session do |session|
32
+ ios = session.listeners.keys
33
+ session.postprocess(ios & readers, ios & writers)
34
+ end
35
+ end
36
+
37
+ true
38
+ end
39
+
40
+ def ensure_each_session
41
+ errors = []
42
+
43
+ sessions.each do |session|
44
+ begin
45
+ yield session
46
+ rescue Exception => error
47
+ errors << SessionAssociation.on(error, session)
48
+ end
49
+ end
50
+
51
+ raise errors.first if errors.any?
52
+ sessions
53
+ end
54
+ end
55
+ end
@@ -7,7 +7,7 @@ load 'deploy'
7
7
  map = { "diff_from_last_deploy" => "deploy:pending:diff",
8
8
  "update" => "deploy:update",
9
9
  "update_code" => "deploy:update_code",
10
- "symlink" => "deploy:symlink",
10
+ "symlink" => "deploy:create_symlink",
11
11
  "restart" => "deploy:restart",
12
12
  "rollback" => "deploy:rollback",
13
13
  "cleanup" => "deploy:cleanup",
@@ -29,4 +29,4 @@ task :spinner do
29
29
  warn "[DEPRECATED] `spinner' is deprecated. Use `deploy:start' instead."
30
30
  set :runner, fetch(:spinner_user, "app")
31
31
  deploy.start
32
- end
32
+ end
@@ -0,0 +1,185 @@
1
+ require 'json'
2
+
3
+ load 'deploy' unless defined?(_cset)
4
+
5
+ _cset :asset_env, "RAILS_GROUPS=assets"
6
+ _cset :assets_prefix, "assets"
7
+ _cset :shared_assets_prefix, "assets"
8
+ _cset :assets_role, [:web]
9
+ _cset :expire_assets_after, (3600 * 24 * 7)
10
+
11
+ _cset :normalize_asset_timestamps, false
12
+
13
+ before 'deploy:finalize_update', 'deploy:assets:symlink'
14
+ after 'deploy:update_code', 'deploy:assets:precompile'
15
+ before 'deploy:assets:precompile', 'deploy:assets:update_asset_mtimes'
16
+ after 'deploy:cleanup', 'deploy:assets:clean_expired'
17
+ after 'deploy:rollback:revision', 'deploy:assets:rollback'
18
+
19
+ def shared_manifest_path
20
+ @shared_manifest_path ||= capture("ls #{shared_path.shellescape}/#{shared_assets_prefix}/manifest*").strip
21
+ end
22
+
23
+ # Parses manifest and returns array of uncompressed and compressed asset filenames with and without digests
24
+ # "Intelligently" determines format of string - supports YAML and JSON
25
+ def parse_manifest(str)
26
+ assets_hash = str[0,1] == '{' ? JSON.parse(str)['assets'] : YAML.load(str)
27
+
28
+ assets_hash.to_a.flatten.map {|a| [a, "#{a}.gz"] }.flatten
29
+ end
30
+
31
+ namespace :deploy do
32
+ namespace :assets do
33
+ desc <<-DESC
34
+ [internal] This task will set up a symlink to the shared directory \
35
+ for the assets directory. Assets are shared across deploys to avoid \
36
+ mid-deploy mismatches between old application html asking for assets \
37
+ and getting a 404 file not found error. The assets cache is shared \
38
+ for efficiency. If you customize the assets path prefix, override the \
39
+ :assets_prefix variable to match. If you customize shared assets path \
40
+ prefix, override :shared_assets_prefix variable to match.
41
+ DESC
42
+ task :symlink, :roles => lambda { assets_role }, :except => { :no_release => true } do
43
+ run <<-CMD.compact
44
+ rm -rf #{latest_release}/public/#{assets_prefix} &&
45
+ mkdir -p #{latest_release}/public &&
46
+ mkdir -p #{shared_path}/#{shared_assets_prefix} &&
47
+ ln -s #{shared_path}/#{shared_assets_prefix} #{latest_release}/public/#{assets_prefix}
48
+ CMD
49
+ end
50
+
51
+ desc <<-DESC
52
+ Run the asset precompilation rake task. You can specify the full path \
53
+ to the rake executable by setting the rake variable. You can also \
54
+ specify additional environment variables to pass to rake via the \
55
+ asset_env variable. The defaults are:
56
+
57
+ set :rake, "rake"
58
+ set :rails_env, "production"
59
+ set :asset_env, "RAILS_GROUPS=assets"
60
+ DESC
61
+ task :precompile, :roles => lambda { assets_role }, :except => { :no_release => true } do
62
+ run <<-CMD.compact
63
+ cd -- #{latest_release} &&
64
+ #{rake} RAILS_ENV=#{rails_env.to_s.shellescape} #{asset_env} assets:precompile
65
+ CMD
66
+
67
+ # Sync manifest filenames across servers if our manifest has a random filename
68
+ if shared_manifest_path =~ /manifest-.+\./
69
+ run <<-CMD.compact
70
+ [ -e #{shared_manifest_path.shellescape} ] || mv -- #{shared_path.shellescape}/#{shared_assets_prefix}/manifest* #{shared_manifest_path.shellescape}
71
+ CMD
72
+ end
73
+
74
+ # Copy manifest to release root (for clean_expired task)
75
+ run <<-CMD.compact
76
+ cp -- #{shared_manifest_path.shellescape} #{current_release.to_s.shellescape}/assets_manifest#{File.extname(shared_manifest_path)}
77
+ CMD
78
+ end
79
+
80
+ desc <<-DESC
81
+ [internal] Updates the mtimes for assets that are required by the current release.
82
+ This task runs before assets:precompile.
83
+ DESC
84
+ task :update_asset_mtimes, :roles => lambda { assets_role }, :except => { :no_release => true } do
85
+ # Fetch assets/manifest contents.
86
+ manifest_content = capture("[ -e #{shared_path.shellescape}/#{shared_assets_prefix}/manifest* ] && cat #{shared_path.shellescape}/#{shared_assets_prefix}/manifest* || echo").strip
87
+
88
+ if manifest_content != ""
89
+ current_assets = parse_manifest(manifest_content)
90
+ logger.info "Updating mtimes for ~#{current_assets.count} assets..."
91
+ put current_assets.map{|a| "#{shared_path}/#{shared_assets_prefix}/#{a}" }.join("\n"), "#{deploy_to}/TOUCH_ASSETS", :via => :scp
92
+ run <<-CMD.compact
93
+ cat #{deploy_to.shellescape}/TOUCH_ASSETS | while read asset; do
94
+ touch -c -- "$asset";
95
+ done &&
96
+ rm -f -- #{deploy_to.shellescape}/TOUCH_ASSETS
97
+ CMD
98
+ end
99
+ end
100
+
101
+ desc <<-DESC
102
+ Run the asset clean rake task. Use with caution, this will delete \
103
+ all of your compiled assets. You can specify the full path \
104
+ to the rake executable by setting the rake variable. You can also \
105
+ specify additional environment variables to pass to rake via the \
106
+ asset_env variable. The defaults are:
107
+
108
+ set :rake, "rake"
109
+ set :rails_env, "production"
110
+ set :asset_env, "RAILS_GROUPS=assets"
111
+ DESC
112
+ task :clean, :roles => lambda { assets_role }, :except => { :no_release => true } do
113
+ run "cd #{latest_release} && #{rake} RAILS_ENV=#{rails_env} #{asset_env} assets:clean"
114
+ end
115
+
116
+ desc <<-DESC
117
+ Clean up any assets that haven't been deployed for more than :expire_assets_after seconds.
118
+ Default time to keep old assets is one week. Set the :expire_assets_after variable
119
+ to change the assets expiry time. Assets will only be deleted if they are not required by
120
+ an existing release.
121
+ DESC
122
+ task :clean_expired, :roles => lambda { assets_role }, :except => { :no_release => true } do
123
+ # Fetch all assets_manifest contents.
124
+ manifests_output = capture <<-CMD.compact
125
+ for manifest in #{releases_path.shellescape}/*/assets_manifest.*; do
126
+ cat -- "$manifest" 2> /dev/null && printf ':::' || true;
127
+ done
128
+ CMD
129
+ manifests = manifests_output.split(':::')
130
+
131
+ if manifests.empty?
132
+ logger.info "No manifests in #{releases_path}/*/assets_manifest.*"
133
+ else
134
+ logger.info "Fetched #{manifests.count} manifests from #{releases_path}/*/assets_manifest.*"
135
+ current_assets = Set.new
136
+ manifests.each do |content|
137
+ current_assets += parse_manifest(content)
138
+ end
139
+ current_assets += [File.basename(shared_manifest_path), "sources_manifest.yml"]
140
+
141
+ # Write the list of required assets to server.
142
+ logger.info "Writing required assets to #{deploy_to}/REQUIRED_ASSETS..."
143
+ escaped_assets = current_assets.sort.join("\n").gsub("\"", "\\\"") << "\n"
144
+ put escaped_assets, "#{deploy_to}/REQUIRED_ASSETS", :via => :scp
145
+
146
+ # Finds all files older than X minutes, then removes them if they are not referenced
147
+ # in REQUIRED_ASSETS.
148
+ expire_after_mins = (expire_assets_after.to_f / 60.0).to_i
149
+ logger.info "Removing assets that haven't been deployed for #{expire_after_mins} minutes..."
150
+ # LC_COLLATE=C tells the `sort` and `comm` commands to sort files in byte order.
151
+ run <<-CMD.compact
152
+ cd -- #{deploy_to.shellescape}/ &&
153
+ LC_COLLATE=C sort REQUIRED_ASSETS -o REQUIRED_ASSETS &&
154
+ cd -- #{shared_path.shellescape}/#{shared_assets_prefix}/ &&
155
+ for f in $(
156
+ find * -mmin +#{expire_after_mins.to_s.shellescape} -type f | LC_COLLATE=C sort |
157
+ LC_COLLATE=C comm -23 -- - #{deploy_to.shellescape}/REQUIRED_ASSETS
158
+ ); do
159
+ echo "Removing unneeded asset: $f";
160
+ rm -f -- "$f";
161
+ done;
162
+ rm -f -- #{deploy_to.shellescape}/REQUIRED_ASSETS
163
+ CMD
164
+ end
165
+ end
166
+
167
+ desc <<-DESC
168
+ Rolls back assets to the previous release by symlinking the release's manifest
169
+ to shared/assets/manifest, and finally recompiling or regenerating nondigest assets.
170
+ DESC
171
+ task :rollback, :roles => lambda { assets_role }, :except => { :no_release => true } do
172
+ previous_manifest = capture("ls #{previous_release.shellescape}/assets_manifest.*").strip
173
+ if capture("[ -e #{previous_manifest.shellescape} ] && echo true || echo false").strip != 'true'
174
+ puts "#{previous_manifest} is missing! Cannot roll back assets. " <<
175
+ "Please run deploy:assets:precompile to update your assets when the rollback is finished."
176
+ else
177
+ run <<-CMD.compact
178
+ cd -- #{previous_release.shellescape} &&
179
+ cp -f -- #{previous_manifest.shellescape} #{shared_manifest_path.shellescape} &&
180
+ [ -z "$(#{rake} -P | grep assets:precompile:nondigest)" ] || #{rake} RAILS_ENV=#{rails_env.to_s.shellescape} #{asset_env} assets:precompile:nondigest
181
+ CMD
182
+ end
183
+ end
184
+ end
185
+ end
@@ -18,7 +18,7 @@ module Capistrano
18
18
  yield self
19
19
  self
20
20
  end
21
-
21
+
22
22
  def remote
23
23
  dep = RemoteDependency.new(configuration)
24
24
  @dependencies << dep
@@ -41,4 +41,4 @@ module Capistrano
41
41
  end
42
42
  end
43
43
  end
44
- end
44
+ end
@@ -30,7 +30,7 @@ module Capistrano
30
30
  # file is found that matches the parameter, this returns true.
31
31
  def find_in_path(utility)
32
32
  path = (ENV['PATH'] || "").split(File::PATH_SEPARATOR)
33
- suffixes = RUBY_PLATFORM =~ /mswin/ ? %w(.bat .exe .com .cmd) : [""]
33
+ suffixes = self.class.on_windows? ? self.class.windows_executable_extensions : [""]
34
34
 
35
35
  path.each do |dir|
36
36
  suffixes.each do |sfx|
@@ -41,6 +41,14 @@ module Capistrano
41
41
 
42
42
  false
43
43
  end
44
+
45
+ def self.on_windows?
46
+ RUBY_PLATFORM =~ /mswin|mingw/
47
+ end
48
+
49
+ def self.windows_executable_extensions
50
+ %w(.exe .bat .com .cmd)
51
+ end
44
52
  end
45
53
  end
46
- end
54
+ end
@@ -1,3 +1,5 @@
1
+ require 'capistrano/errors'
2
+
1
3
  module Capistrano
2
4
  module Deploy
3
5
  class RemoteDependency
@@ -7,6 +9,7 @@ module Capistrano
7
9
  def initialize(configuration)
8
10
  @configuration = configuration
9
11
  @success = true
12
+ @hosts = nil
10
13
  end
11
14
 
12
15
  def directory(path, options={})
@@ -15,6 +18,12 @@ module Capistrano
15
18
  self
16
19
  end
17
20
 
21
+ def file(path, options={})
22
+ @message ||= "`#{path}' is not a file"
23
+ try("test -f #{path}", options)
24
+ self
25
+ end
26
+
18
27
  def writable(path, options={})
19
28
  @message ||= "`#{path}' is not writable"
20
29
  try("test -w #{path}", options)
@@ -34,6 +43,48 @@ module Capistrano
34
43
  self
35
44
  end
36
45
 
46
+ def deb(name, version, options={})
47
+ @message ||= "package `#{name}' #{version} could not be found"
48
+ try("dpkg -s #{name} | grep '^Version: #{version}'", options)
49
+ self
50
+ end
51
+
52
+ def rpm(name, version, options={})
53
+ @message ||= "package `#{name}' #{version} could not be found"
54
+ try("rpm -q #{name} | grep '#{version}'", options)
55
+ self
56
+ end
57
+
58
+ def match(command, expect, options={})
59
+ expect = Regexp.new(Regexp.escape(expect.to_s)) unless expect.is_a?(Regexp)
60
+
61
+ output_per_server = {}
62
+ try("#{command} ", options) do |ch, stream, out|
63
+ output_per_server[ch[:server]] ||= ''
64
+ output_per_server[ch[:server]] += out
65
+ end
66
+
67
+ # It is possible for some of these commands to return a status != 0
68
+ # (for example, rake --version exits with a 1). For this check we
69
+ # just care if the output matches, so we reset the success flag.
70
+ @success = true
71
+
72
+ errored_hosts = []
73
+ output_per_server.each_pair do |server, output|
74
+ next if output =~ expect
75
+ errored_hosts << server
76
+ end
77
+
78
+ if errored_hosts.any?
79
+ @hosts = errored_hosts.join(', ')
80
+ output = output_per_server[errored_hosts.first]
81
+ @message = "the output #{output.inspect} from #{command.inspect} did not match #{expect.inspect}"
82
+ @success = false
83
+ end
84
+
85
+ self
86
+ end
87
+
37
88
  def or(message)
38
89
  @message = message
39
90
  self
@@ -45,7 +96,7 @@ module Capistrano
45
96
 
46
97
  def message
47
98
  s = @message.dup
48
- s << " (#{@hosts})" if @hosts && @hosts.any?
99
+ s << " (#{@hosts})" if @hosts
49
100
  s
50
101
  end
51
102
 
@@ -53,8 +104,9 @@ module Capistrano
53
104
 
54
105
  def try(command, options)
55
106
  return unless @success # short-circuit evaluation
56
- configuration.run(command, options) do |ch,stream,out|
107
+ configuration.invoke_command(command, options) do |ch,stream,out|
57
108
  warn "#{ch[:server]}: #{out}" if stream == :err
109
+ yield ch, stream, out if block_given?
58
110
  end
59
111
  rescue Capistrano::CommandError => e
60
112
  @success = false