capistrano 2.3.0 → 2.4.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 (35) hide show
  1. data/CHANGELOG +50 -2
  2. data/lib/capistrano/cli/execute.rb +1 -0
  3. data/lib/capistrano/cli/options.rb +4 -0
  4. data/lib/capistrano/cli/ui.rb +13 -0
  5. data/lib/capistrano/command.rb +2 -2
  6. data/lib/capistrano/configuration.rb +2 -1
  7. data/lib/capistrano/configuration/actions/file_transfer.rb +5 -1
  8. data/lib/capistrano/configuration/actions/invocation.rb +45 -18
  9. data/lib/capistrano/configuration/callbacks.rb +3 -3
  10. data/lib/capistrano/configuration/execution.rb +8 -3
  11. data/lib/capistrano/configuration/loading.rb +20 -21
  12. data/lib/capistrano/recipes/deploy.rb +56 -27
  13. data/lib/capistrano/recipes/deploy/local_dependency.rb +6 -2
  14. data/lib/capistrano/recipes/deploy/remote_dependency.rb +2 -0
  15. data/lib/capistrano/recipes/deploy/scm/git.rb +21 -12
  16. data/lib/capistrano/recipes/deploy/scm/mercurial.rb +2 -1
  17. data/lib/capistrano/recipes/deploy/scm/subversion.rb +2 -1
  18. data/lib/capistrano/recipes/deploy/strategy/copy.rb +22 -34
  19. data/lib/capistrano/recipes/deploy/strategy/remote.rb +1 -1
  20. data/lib/capistrano/version.rb +1 -14
  21. data/test/cli/execute_test.rb +1 -1
  22. data/test/cli/options_test.rb +7 -1
  23. data/test/configuration/actions/file_transfer_test.rb +20 -1
  24. data/test/configuration/actions/invocation_test.rb +16 -10
  25. data/test/configuration/callbacks_test.rb +16 -2
  26. data/test/configuration/loading_test.rb +6 -1
  27. data/test/deploy/local_dependency_test.rb +73 -0
  28. data/test/deploy/remote_dependency_test.rb +114 -0
  29. data/test/deploy/scm/git_test.rb +22 -8
  30. data/test/deploy/scm/mercurial_test.rb +10 -4
  31. data/test/deploy/strategy/copy_test.rb +16 -11
  32. data/test/role_test.rb +11 -0
  33. data/test/server_definition_test.rb +14 -1
  34. metadata +5 -3
  35. data/test/version_test.rb +0 -24
data/CHANGELOG CHANGED
@@ -1,6 +1,54 @@
1
- *2.3.0* May 2, 2008
1
+ *2.4.0* June 13, 2008
2
+
3
+ * Added :normalize_asset_timestamps option to deployment, defaulting to true, which allows asset timestamping to be disabled [John Trupiano]
4
+
5
+
6
+ *2.4.0 Preview Release #1* (2.3.101) June 5, 2008
7
+
8
+ * Only make deploy:start, deploy:stop, and deploy:restart try sudo as :runner. The other sudo-enabled tasks (deploy:setup, deploy:cleanup, etc.) will now use the :admin_runner user (which by default is unset). [Jamis Buck]
9
+
10
+ * Make sure triggers defined as a block inherit the scope of the task they are attached to, instead of the task they were called from [Jamis Buck]
11
+
12
+ * Make deploy:upload use the upload() helper for more efficient directory processing [Jamis Buck]
13
+
14
+ * Make deploy:upload accept globs [Mark Imbriaco]
15
+
16
+ * Make sure the host is reported with the output from scm_run [Jamis Buck]
17
+
18
+ * Make git SCM honor the :scm_verbose option [Jamis Buck]
19
+
20
+ * Don't follow symlinks when using :copy_cache [Jamis Buck]
21
+
22
+ * If :mode is given to upload() helper, do a chmod after to set the mode [Jamis Buck]
23
+
24
+ * Fix load_from_file method for windows users [Neil Wilson]
25
+
26
+ * Display a deprecation error if a remote git branch is specified [Tim Harper]
2
27
 
3
- * Make sure git fetches include tags [Alex Arnell]
28
+ * Fix deployment recipes to use the updated sudo helper [Jamis Buck]
29
+
30
+ * Enhance the sudo helper so it can be used to return the command, instead of executing it [Jamis Buck]
31
+
32
+ * Revert "make sudo helper play nicely with complex command chains", since it broke stuff [Jamis Buck]
33
+
34
+ * Make set(:default_shell, false) work for not using a shell on a per-command basis [Ryan McGeary]
35
+
36
+ * Improved test coverage [Ryan McGeary]
37
+
38
+ * Fixed "coverage" take task [Ryan McGeary]
39
+
40
+ * Use upload() instead of put() with the copy strategy [Jamis Buck]
41
+
42
+ * Revert the "git fetch --tags" change, since it didn't work as expected [Jamis Buck]
43
+
44
+ * Fix deploy:pending when using git SCM [Ryan McGeary]
45
+
46
+ * Make sure deploy:check works with :none scm (which has no default command) [Jamis Buck]
47
+
48
+ * Add debug switch for enabling conditional execution of commands [Mark Imbriaco]
49
+
50
+
51
+ *2.3.0* May 2, 2008
4
52
 
5
53
  * Make deploy:setup obey the :use_sudo and :runner directives, and generalize the :use_sudo and :runner options into a try_sudo() helper method [Jamis Buck]
6
54
 
@@ -22,6 +22,7 @@ module Capistrano
22
22
  # Returns the Configuration instance used, if successful.
23
23
  def execute!
24
24
  config = instantiate_configuration
25
+ config.debug = options[:debug]
25
26
  config.logger.level = options[:verbose]
26
27
 
27
28
  set_pre_vars(config)
@@ -27,6 +27,10 @@ module Capistrano
27
27
  @option_parser ||= OptionParser.new do |opts|
28
28
  opts.banner = "Usage: #{File.basename($0)} [options] action ..."
29
29
 
30
+ opts.on("-d", "--debug",
31
+ "Prompts before each remote command execution."
32
+ ) { |value| options[:debug] = true }
33
+
30
34
  opts.on("-e", "--explain TASK",
31
35
  "Displays help (if available) for the task."
32
36
  ) { |value| options[:explain] = value }
@@ -22,6 +22,19 @@ module Capistrano
22
22
  def password_prompt(prompt="Password: ")
23
23
  ui.ask(prompt) { |q| q.echo = false }
24
24
  end
25
+
26
+ # Debug mode prompt
27
+ def debug_prompt(cmd)
28
+ ui.say("Preparing to execute command: #{cmd}")
29
+ prompt = "Execute ([Yes], No, Abort) "
30
+ ui.ask("#{prompt}? ") do |q|
31
+ q.overwrite = false
32
+ q.character = true
33
+ q.default = 'y'
34
+ q.validate = /(y(es)?)|(no?)|(a(bort)?|\n)/i
35
+ q.responses[:not_valid] = prompt
36
+ end
37
+ end
25
38
  end
26
39
  end
27
40
  end
@@ -82,12 +82,12 @@ module Capistrano
82
82
  if options[:shell] == false
83
83
  shell = nil
84
84
  else
85
- shell = [options.fetch(:shell, "sh"), "-c"].join(" ")
85
+ shell = "#{options[:shell] || "sh"} -c"
86
86
  cmd = cmd.gsub(/[$\\`"]/) { |m| "\\#{m}" }
87
87
  cmd = "\"#{cmd}\""
88
88
  end
89
89
 
90
- command_line = [environment, options[:command_prefix], shell, cmd].compact.join(" ")
90
+ command_line = [environment, shell, cmd].compact.join(" ")
91
91
 
92
92
  ch.exec(command_line)
93
93
  ch.send_data(options[:data]) if options[:data]
@@ -19,9 +19,10 @@ module Capistrano
19
19
  # define roles, and set configuration variables.
20
20
  class Configuration
21
21
  # The logger instance defined for this configuration.
22
- attr_accessor :logger
22
+ attr_accessor :debug, :logger
23
23
 
24
24
  def initialize #:nodoc:
25
+ @debug = false
25
26
  @logger = Logger.new
26
27
  end
27
28
 
@@ -10,7 +10,6 @@ module Capistrano
10
10
  # set the mode on the file.
11
11
  def put(data, path, options={})
12
12
  opts = options.dup
13
- opts[:permissions] = opts.delete(:mode)
14
13
  upload(StringIO.new(data), path, opts)
15
14
  end
16
15
 
@@ -23,7 +22,12 @@ module Capistrano
23
22
  end
24
23
 
25
24
  def upload(from, to, options={}, &block)
25
+ mode = options.delete(:mode)
26
26
  transfer(:up, from, to, options, &block)
27
+ if mode
28
+ mode = mode.is_a?(Numeric) ? mode.to_s(8) : mode.to_s
29
+ run "chmod #{mode} #{to}"
30
+ end
27
31
  end
28
32
 
29
33
  def download(from, to, options={}, &block)
@@ -46,33 +46,49 @@ module Capistrano
46
46
  block ||= self.class.default_io_proc
47
47
  logger.debug "executing #{cmd.strip.inspect}"
48
48
 
49
+ return if debug && continue_execution(cmd) == false
50
+
49
51
  options = add_default_command_options(options)
50
52
 
53
+ if cmd.include?(sudo)
54
+ block = sudo_behavior_callback(block)
55
+ end
56
+
51
57
  execute_on_servers(options) do |servers|
52
58
  targets = servers.map { |s| sessions[s] }
53
59
  Command.process(cmd, targets, options.merge(:logger => logger), &block)
54
60
  end
55
61
  end
56
62
 
57
- # Like #run, but executes the command via <tt>sudo</tt>. This assumes
58
- # that the sudo password (if required) is the same as the password for
59
- # logging in to the server.
63
+ # Returns the command string used by capistrano to invoke a comamnd via
64
+ # sudo.
65
+ #
66
+ # run "#{sudo :as => 'bob'} mkdir /path/to/dir"
60
67
  #
61
- # Also, this module accepts a <tt>:sudo</tt> configuration variable,
68
+ # It can also be invoked like #run, but executing the command via sudo.
69
+ # This assumes that the sudo password (if required) is the same as the
70
+ # password for logging in to the server.
71
+ #
72
+ # sudo "mkdir /path/to/dir"
73
+ #
74
+ # Also, this method understands a <tt>:sudo</tt> configuration variable,
62
75
  # which (if specified) will be used as the full path to the sudo
63
76
  # executable on the remote machine:
64
77
  #
65
78
  # set :sudo, "/opt/local/bin/sudo"
66
- def sudo(command, options={}, &block)
67
- block ||= self.class.default_io_proc
68
-
69
- options = options.dup
70
- as = options.delete(:as)
71
-
72
- user = as && "-u #{as}"
73
- options[:command_prefix] = [fetch(:sudo, "sudo"), "-p '#{sudo_prompt}'", user].compact.join(" ")
74
-
75
- run(command, options, &sudo_behavior_callback(block))
79
+ def sudo(*parameters, &block)
80
+ options = parameters.last.is_a?(Hash) ? parameters.pop.dup : {}
81
+ command = parameters.first
82
+ user = options[:as] && "-u #{options.delete(:as)}"
83
+
84
+ sudo_command = [fetch(:sudo, "sudo"), "-p '#{sudo_prompt}'", user].compact.join(" ")
85
+
86
+ if command
87
+ command = sudo_command + " " + command
88
+ run(command, options, &block)
89
+ else
90
+ return sudo_command
91
+ end
76
92
  end
77
93
 
78
94
  # Returns a Proc object that defines the behavior of the sudo
@@ -88,13 +104,13 @@ module Capistrano
88
104
  Proc.new do |ch, stream, out|
89
105
  if out =~ /^#{Regexp.escape(sudo_prompt)}/
90
106
  ch.send_data "#{self[:password]}\n"
91
- elsif out =~ /try again/
107
+ elsif out =~ /^Sorry, try again/
92
108
  if prompt_host.nil? || prompt_host == ch[:server]
93
109
  prompt_host = ch[:server]
94
110
  logger.important out, "#{stream} :: #{ch[:server]}"
95
111
  reset! :password
96
112
  end
97
- else
113
+ elsif fallback
98
114
  fallback.call(ch, stream, out)
99
115
  end
100
116
  end
@@ -119,7 +135,7 @@ module Capistrano
119
135
  options[:env] = env unless env.empty?
120
136
 
121
137
  shell = options[:shell] || self[:default_shell]
122
- options[:shell] = shell if shell
138
+ options[:shell] = shell unless shell.nil?
123
139
 
124
140
  options
125
141
  end
@@ -128,7 +144,18 @@ module Capistrano
128
144
  def sudo_prompt
129
145
  fetch(:sudo_prompt, "sudo password: ")
130
146
  end
147
+
148
+ def continue_execution(cmd)
149
+ case Capistrano::CLI.debug_prompt(cmd)
150
+ when "y"
151
+ true
152
+ when "n"
153
+ false
154
+ when "a"
155
+ exit(-1)
156
+ end
157
+ end
131
158
  end
132
159
  end
133
160
  end
134
- end
161
+ end
@@ -4,7 +4,7 @@ module Capistrano
4
4
  class Configuration
5
5
  module Callbacks
6
6
  def self.included(base) #:nodoc:
7
- %w(initialize execute_task).each do |method|
7
+ %w(initialize invoke_task_directly).each do |method|
8
8
  base.send :alias_method, "#{method}_without_callbacks", method
9
9
  base.send :alias_method, method, "#{method}_with_callbacks"
10
10
  end
@@ -18,13 +18,13 @@ module Capistrano
18
18
  @callbacks = {}
19
19
  end
20
20
 
21
- def execute_task_with_callbacks(task) #:nodoc:
21
+ def invoke_task_directly_with_callbacks(task) #:nodoc:
22
22
  before = find_hook(task, :before)
23
23
  execute_task(before) if before
24
24
 
25
25
  trigger :before, task
26
26
 
27
- result = execute_task_without_callbacks(task)
27
+ result = invoke_task_directly_without_callbacks(task)
28
28
 
29
29
  trigger :after, task
30
30
 
@@ -72,12 +72,12 @@ module Capistrano
72
72
  task_call_frames.last.task
73
73
  end
74
74
 
75
- # Executes the task with the given name, including the before and after
76
- # hooks.
75
+ # Executes the task with the given name, without invoking any associated
76
+ # callbacks.
77
77
  def execute_task(task)
78
78
  logger.debug "executing `#{task.fully_qualified_name}'"
79
79
  push_task_call_frame(task)
80
- task.namespace.instance_eval(&task.body)
80
+ invoke_task_directly(task)
81
81
  ensure
82
82
  pop_task_call_frame
83
83
  end
@@ -121,6 +121,11 @@ module Capistrano
121
121
  def pop_task_call_frame
122
122
  task_call_frames.pop
123
123
  end
124
+
125
+ # Invokes the task's body directly, without setting up the call frame.
126
+ def invoke_task_directly(task)
127
+ task.namespace.instance_eval(&task.body)
128
+ end
124
129
  end
125
130
  end
126
131
  end
@@ -140,27 +140,26 @@ module Capistrano
140
140
  def require(*args) #:nodoc:
141
141
  # look to see if this specific configuration instance has ever seen
142
142
  # these arguments to require before
143
- if !@loaded_features.include?(args)
144
- @loaded_features << args
145
-
146
- begin
147
- original_instance, self.class.instance = self.class.instance, self
148
- original_feature, self.class.current_feature = self.class.current_feature, args
149
-
150
- result = super
151
- if !result # file has been required previously, load up the remembered recipes
152
- list = self.class.recipes_per_feature[args] || []
153
- list.each { |options| load(options.merge(:reloading => true)) }
154
- end
155
-
156
- return result
157
- ensure
158
- # restore the original, so that require's can be nested
159
- self.class.instance = original_instance
160
- self.class.current_feature = original_feature
143
+ if @loaded_features.include?(args)
144
+ return false
145
+ end
146
+
147
+ @loaded_features << args
148
+ begin
149
+ original_instance, self.class.instance = self.class.instance, self
150
+ original_feature, self.class.current_feature = self.class.current_feature, args
151
+
152
+ result = super
153
+ if !result # file has been required previously, load up the remembered recipes
154
+ list = self.class.recipes_per_feature[args] || []
155
+ list.each { |options| load(options.merge(:reloading => true)) }
161
156
  end
162
- else
163
- return false
157
+
158
+ return result
159
+ ensure
160
+ # restore the original, so that require's can be nested
161
+ self.class.instance = original_instance
162
+ self.class.current_feature = original_feature
164
163
  end
165
164
  end
166
165
 
@@ -169,7 +168,7 @@ module Capistrano
169
168
  # Load a recipe from the named file. If +name+ is given, the file will
170
169
  # be reported using that name.
171
170
  def load_from_file(file, name=nil)
172
- file = find_file_in_load_path(file) unless file[0] == ?/
171
+ file = find_file_in_load_path(file) unless File.file?(file)
173
172
  load :string => File.read(file), :name => name || file
174
173
  end
175
174
 
@@ -89,13 +89,43 @@ ensure
89
89
  ENV[name] = saved
90
90
  end
91
91
 
92
+ # If a command is given, this will try to execute the given command, as
93
+ # described below. Otherwise, it will return a string for use in embedding in
94
+ # another command, for executing that command as described below.
95
+ #
92
96
  # If :run_method is :sudo (or :use_sudo is true), this executes the given command
93
- # via +sudo+. Otherwise is uses +run+. Further, if sudo is being used and :runner
94
- # is set, the command will be executed as the user given by :runner.
95
- def try_sudo(command)
96
- as = fetch(:runner, "app")
97
+ # via +sudo+. Otherwise is uses +run+. If :as is given as a key, it will be
98
+ # passed as the user to sudo as, if using sudo. If the :as key is not given,
99
+ # it will default to whatever the value of the :admin_runner variable is,
100
+ # which (by default) is unset.
101
+ #
102
+ # THUS, if you want to try to run something via sudo, and what to use the
103
+ # root user, you'd just to try_sudo('something'). If you wanted to try_sudo as
104
+ # someone else, you'd just do try_sudo('something', :as => "bob"). If you
105
+ # always wanted sudo to run as a particular user, you could do
106
+ # set(:admin_runner, "bob").
107
+ def try_sudo(*args)
108
+ options = args.last.is_a?(Hash) ? args.pop : {}
109
+ command = args.shift
110
+ raise ArgumentError, "too many arguments" if args.any?
111
+
112
+ as = options.fetch(:as, fetch(:admin_runner, nil))
97
113
  via = fetch(:run_method, :sudo)
98
- invoke_command(command, :via => via, :as => as)
114
+ if command
115
+ invoke_command(command, :via => via, :as => as)
116
+ elsif via == :sudo
117
+ sudo(:as => as)
118
+ else
119
+ ""
120
+ end
121
+ end
122
+
123
+ # Same as sudo, but tries sudo with :as set to the value of the :runner
124
+ # variable (which defaults to "app").
125
+ def try_runner(*args)
126
+ options = args.last.is_a?(Hash) ? args.pop : {}
127
+ args << options.merge(:as => fetch(:runner, "app"))
128
+ try_sudo(*args)
99
129
  end
100
130
 
101
131
  # =========================================================================
@@ -131,7 +161,7 @@ namespace :deploy do
131
161
  task :setup, :except => { :no_release => true } do
132
162
  dirs = [deploy_to, releases_path, shared_path]
133
163
  dirs += %w(system log pids).map { |d| File.join(shared_path, d) }
134
- try_sudo "umask 02 && mkdir -p #{dirs.join(' ')}"
164
+ run "#{try_sudo} mkdir -p #{dirs.join(' ')} && #{try_sudo} chmod g+w #{dirs.join(' ')}"
135
165
  end
136
166
 
137
167
  desc <<-DESC
@@ -178,7 +208,9 @@ namespace :deploy do
178
208
  symlinks to the shared directory for the log, system, and tmp/pids \
179
209
  directories, and will lastly touch all assets in public/images, \
180
210
  public/stylesheets, and public/javascripts so that the times are \
181
- consistent (so that asset timestamping works).
211
+ consistent (so that asset timestamping works). This touch process \
212
+ is only carried out if the :normalize_asset_timestamps variable is \
213
+ set to true, which is the default.
182
214
  DESC
183
215
  task :finalize_update, :except => { :no_release => true } do
184
216
  run "chmod -R g+w #{latest_release}" if fetch(:group_writable, true)
@@ -194,9 +226,11 @@ namespace :deploy do
194
226
  ln -s #{shared_path}/pids #{latest_release}/tmp/pids
195
227
  CMD
196
228
 
197
- stamp = Time.now.utc.strftime("%Y%m%d%H%M.%S")
198
- asset_paths = %w(images stylesheets javascripts).map { |p| "#{latest_release}/public/#{p}" }.join(" ")
199
- run "find #{asset_paths} -exec touch -t #{stamp} {} ';'; true", :env => { "TZ" => "UTC" }
229
+ if fetch(:normalize_asset_timestamps, true)
230
+ stamp = Time.now.utc.strftime("%Y%m%d%H%M.%S")
231
+ asset_paths = %w(images stylesheets javascripts).map { |p| "#{latest_release}/public/#{p}" }.join(" ")
232
+ run "find #{asset_paths} -exec touch -t #{stamp} {} ';'; true", :env => { "TZ" => "UTC" }
233
+ end
200
234
  end
201
235
 
202
236
  desc <<-DESC
@@ -223,24 +257,19 @@ namespace :deploy do
223
257
  To use this task, specify the files and directories you want to copy as a \
224
258
  comma-delimited list in the FILES environment variable. All directories \
225
259
  will be processed recursively, with all files being pushed to the \
226
- deployment servers. Any file or directory starting with a '.' character \
227
- will be ignored.
260
+ deployment servers.
228
261
 
229
262
  $ cap deploy:upload FILES=templates,controller.rb
263
+
264
+ Dir globs are also supported:
265
+
266
+ $ cap deploy:upload FILES='config/apache/*.conf'
230
267
  DESC
231
268
  task :upload, :except => { :no_release => true } do
232
- files = (ENV["FILES"] || "").
233
- split(",").
234
- map { |f| f.strip!; File.directory?(f) ? Dir["#{f}/**/*"] : f }.
235
- flatten.
236
- reject { |f| File.directory?(f) || File.basename(f)[0] == ?. }
269
+ files = (ENV["FILES"] || "").split(",").map { |f| Dir[f.strip] }.flatten
270
+ abort "Please specify at least one file or directory to update (via the FILES environment variable)" if files.empty?
237
271
 
238
- abort "Please specify at least one file to update (via the FILES environment variable)" if files.empty?
239
-
240
- files.each do |file|
241
- content = File.open(file, "rb") { |f| f.read }
242
- put content, File.join(current_path, file)
243
- end
272
+ files.each { |file| top.upload(file, File.join(current_path, file)) }
244
273
  end
245
274
 
246
275
  desc <<-DESC
@@ -255,7 +284,7 @@ namespace :deploy do
255
284
  set :use_sudo, false
256
285
  DESC
257
286
  task :restart, :roles => :app, :except => { :no_release => true } do
258
- try_sudo "#{current_path}/script/process/reaper"
287
+ try_runner "#{current_path}/script/process/reaper"
259
288
  end
260
289
 
261
290
  desc <<-DESC
@@ -413,7 +442,7 @@ namespace :deploy do
413
442
  the :use_sudo variable to false.
414
443
  DESC
415
444
  task :start, :roles => :app do
416
- try_sudo "sh -c 'cd #{current_path} && nohup script/spin'"
445
+ run "cd #{current_path} && #{try_runner} nohup script/spin"
417
446
  end
418
447
 
419
448
  desc <<-DESC
@@ -428,8 +457,8 @@ namespace :deploy do
428
457
  the :use_sudo variable to false.
429
458
  DESC
430
459
  task :stop, :roles => :app do
431
- try_sudo "if [ -f #{current_path}/tmp/pids/dispatch.spawner.pid ]; then #{current_path}/script/process/reaper -a kill -r dispatch.spawner.pid; fi"
432
- try_sudo "#{current_path}/script/process/reaper -a kill"
460
+ run "if [ -f #{current_path}/tmp/pids/dispatch.spawner.pid ]; then #{try_runner} #{current_path}/script/process/reaper -a kill -r dispatch.spawner.pid; fi"
461
+ try_runner "#{current_path}/script/process/reaper -a kill"
433
462
  end
434
463
 
435
464
  namespace :pending do