capistrano 2.3.0 → 2.4.0

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