capistrano 1.1.0 → 1.2.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.
- data/CHANGELOG +241 -0
- data/MIT-LICENSE +20 -0
- data/README +35 -0
- data/THANKS +4 -0
- data/lib/capistrano/actor.rb +111 -24
- data/lib/capistrano/cli.rb +18 -9
- data/lib/capistrano/command.rb +8 -0
- data/lib/capistrano/configuration.rb +1 -1
- data/lib/capistrano/gateway.rb +29 -43
- data/lib/capistrano/recipes/standard.rb +38 -10
- data/lib/capistrano/scm/base.rb +1 -2
- data/lib/capistrano/scm/cvs.rb +5 -0
- data/lib/capistrano/scm/mercurial.rb +83 -0
- data/lib/capistrano/scm/subversion.rb +18 -18
- data/lib/capistrano/shell.rb +224 -0
- data/lib/capistrano/ssh.rb +2 -3
- data/lib/capistrano/version.rb +2 -2
- data/test/actor_test.rb +104 -5
- data/test/scm/cvs_test.rb +10 -0
- data/test/scm/subversion_test.rb +6 -6
- metadata +95 -79
data/lib/capistrano/cli.rb
CHANGED
@@ -110,6 +110,11 @@ module Capistrano
|
|
110
110
|
"be specified, and are loaded in the given order."
|
111
111
|
) { |value| @options[:actions] << value }
|
112
112
|
|
113
|
+
opts.on("-f", "--file FILE",
|
114
|
+
"A recipe file to load. Multiple recipes may",
|
115
|
+
"be specified, and are loaded in the given order."
|
116
|
+
) { |value| @options[:recipes] << value }
|
117
|
+
|
113
118
|
opts.on("-p", "--password [PASSWORD]",
|
114
119
|
"The password to use when connecting. If the switch",
|
115
120
|
"is given without a password, the password will be",
|
@@ -119,8 +124,12 @@ module Capistrano
|
|
119
124
|
|
120
125
|
opts.on("-r", "--recipe RECIPE",
|
121
126
|
"A recipe file to load. Multiple recipes may",
|
122
|
-
"be specified, and are loaded in the given order."
|
123
|
-
|
127
|
+
"be specified, and are loaded in the given order.",
|
128
|
+
"(This option is deprecated--please use -f instead)"
|
129
|
+
) do |value|
|
130
|
+
warn "Deprecated -r/--recipe flag used. Please use -f instead"
|
131
|
+
@options[:recipes] << value
|
132
|
+
end
|
124
133
|
|
125
134
|
opts.on("-s", "--set NAME=VALUE",
|
126
135
|
"Specify a variable and it's value to set. This",
|
@@ -188,13 +197,14 @@ You can use the --apply-to switch to generate a minimal set of capistrano
|
|
188
197
|
scripts and recipes for an application. Just specify the path to the application
|
189
198
|
as the argument to --apply-to, like this:
|
190
199
|
|
191
|
-
|
200
|
+
cap --apply-to ~/projects/myapp
|
192
201
|
|
193
|
-
You'll wind up with a sample deployment recipe in config/deploy.rb
|
194
|
-
rake tasks in config/tasks
|
202
|
+
You'll wind up with a sample deployment recipe in config/deploy.rb and some new
|
203
|
+
rake tasks in config/tasks.
|
195
204
|
|
196
205
|
(Currently, --apply-to only works with Rails applications.)
|
197
206
|
DETAIL
|
207
|
+
#' # vim syntax highlighting fix
|
198
208
|
|
199
209
|
if args.empty?
|
200
210
|
puts opts
|
@@ -217,10 +227,10 @@ DETAIL
|
|
217
227
|
|
218
228
|
# Beginning running Capistrano based on the configured options.
|
219
229
|
def execute!
|
220
|
-
if
|
221
|
-
execute_recipes!
|
222
|
-
elsif @options[:apply_to]
|
230
|
+
if @options[:apply_to]
|
223
231
|
execute_apply_to!
|
232
|
+
else
|
233
|
+
execute_recipes!
|
224
234
|
end
|
225
235
|
end
|
226
236
|
|
@@ -271,7 +281,6 @@ DETAIL
|
|
271
281
|
elsif !apply_to_given
|
272
282
|
look_for_default_recipe_file! if @options[:recipes].empty?
|
273
283
|
look_for_raw_actions!
|
274
|
-
abort "You must specify at least one recipe" if @options[:recipes].empty?
|
275
284
|
abort "You must specify at least one action" if @options[:actions].empty?
|
276
285
|
else
|
277
286
|
@options[:application] = args.shift
|
data/lib/capistrano/command.rb
CHANGED
@@ -48,6 +48,14 @@ module Capistrano
|
|
48
48
|
self
|
49
49
|
end
|
50
50
|
|
51
|
+
# Force the command to stop processing, by closing all open channels
|
52
|
+
# associated with this command.
|
53
|
+
def stop!
|
54
|
+
@channels.each do |ch|
|
55
|
+
ch.close unless ch[:closed]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
51
59
|
private
|
52
60
|
|
53
61
|
def open_channels
|
@@ -150,7 +150,7 @@ module Capistrano
|
|
150
150
|
instance_eval(options[:string], options[:name] || "<eval>")
|
151
151
|
|
152
152
|
elsif options[:proc]
|
153
|
-
logger.trace "loading configuration #{options[:proc]
|
153
|
+
logger.trace "loading configuration #{eval("__FILE__", options[:proc])}"
|
154
154
|
instance_eval(&options[:proc])
|
155
155
|
|
156
156
|
else
|
data/lib/capistrano/gateway.rb
CHANGED
@@ -31,28 +31,23 @@ module Capistrano
|
|
31
31
|
def initialize(server, config) #:nodoc:
|
32
32
|
@config = config
|
33
33
|
@pending_forward_requests = {}
|
34
|
-
@mutex = Mutex.new
|
35
34
|
@next_port = MAX_PORT
|
36
35
|
@terminate_thread = false
|
36
|
+
@port_guard = Mutex.new
|
37
37
|
|
38
|
+
mutex = Mutex.new
|
38
39
|
waiter = ConditionVariable.new
|
39
40
|
|
40
41
|
@thread = Thread.new do
|
41
42
|
@config.logger.trace "starting connection to gateway #{server}"
|
42
43
|
SSH.connect(server, @config) do |@session|
|
43
44
|
@config.logger.trace "gateway connection established"
|
44
|
-
|
45
|
-
|
46
|
-
loop do
|
47
|
-
break if @terminate_thread
|
48
|
-
sleep 0.1 unless connection.reader_ready?
|
49
|
-
connection.process true
|
50
|
-
Thread.new { process_next_pending_connection_request }
|
51
|
-
end
|
45
|
+
mutex.synchronize { waiter.signal }
|
46
|
+
@session.loop { !@terminate_thread }
|
52
47
|
end
|
53
48
|
end
|
54
49
|
|
55
|
-
|
50
|
+
mutex.synchronize { waiter.wait(mutex) }
|
56
51
|
end
|
57
52
|
|
58
53
|
# Shuts down all forwarded connections and terminates the gateway.
|
@@ -73,45 +68,36 @@ module Capistrano
|
|
73
68
|
# host to the server, via the gateway, and then opens and returns a new
|
74
69
|
# Net::SSH connection via that port.
|
75
70
|
def connect_to(server)
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
71
|
+
connection = nil
|
72
|
+
@config.logger.trace "establishing connection to #{server} via gateway"
|
73
|
+
port = next_port
|
74
|
+
|
75
|
+
thread = Thread.new do
|
76
|
+
begin
|
77
|
+
@session.forward.local(port, server, 22)
|
78
|
+
connection = SSH.connect('127.0.0.1', @config, port)
|
79
|
+
@config.logger.trace "connection to #{server} via gateway established"
|
80
|
+
rescue Errno::EADDRINUSE
|
81
|
+
port = next_port
|
82
|
+
retry
|
83
|
+
rescue Exception => e
|
84
|
+
puts e.class.name
|
85
|
+
puts e.backtrace.join("\n")
|
86
|
+
end
|
80
87
|
end
|
88
|
+
|
89
|
+
thread.join
|
90
|
+
connection or raise "Could not establish connection to #{server}"
|
81
91
|
end
|
82
92
|
|
83
93
|
private
|
84
94
|
|
85
95
|
def next_port
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
def process_next_pending_connection_request
|
93
|
-
@mutex.synchronize do
|
94
|
-
key = @pending_forward_requests.keys.detect { |k| ConditionVariable === @pending_forward_requests[k] } or return
|
95
|
-
var = @pending_forward_requests[key]
|
96
|
-
|
97
|
-
@config.logger.trace "establishing connection to #{key} via gateway"
|
98
|
-
|
99
|
-
port = next_port
|
100
|
-
|
101
|
-
begin
|
102
|
-
@session.forward.local(port, key, 22)
|
103
|
-
@pending_forward_requests[key] = SSH.connect('127.0.0.1', @config,
|
104
|
-
port)
|
105
|
-
@config.logger.trace "connection to #{key} via gateway established"
|
106
|
-
rescue Errno::EADDRINUSE
|
107
|
-
port = next_port
|
108
|
-
retry
|
109
|
-
rescue Object
|
110
|
-
@pending_forward_requests[key] = nil
|
111
|
-
raise
|
112
|
-
ensure
|
113
|
-
var.signal
|
114
|
-
end
|
96
|
+
@port_guard.synchronize do
|
97
|
+
port = @next_port
|
98
|
+
@next_port -= 1
|
99
|
+
@next_port = MAX_PORT if @next_port < MIN_PORT
|
100
|
+
port
|
115
101
|
end
|
116
102
|
end
|
117
103
|
end
|
@@ -35,10 +35,11 @@ task :show_tasks do
|
|
35
35
|
end
|
36
36
|
|
37
37
|
desc "Set up the expected application directory structure on all boxes"
|
38
|
-
task :setup, :
|
38
|
+
task :setup, :except => { :no_release => true } do
|
39
39
|
run <<-CMD
|
40
40
|
mkdir -p -m 775 #{releases_path} #{shared_path}/system &&
|
41
|
-
mkdir -p -m 777 #{shared_path}/log
|
41
|
+
mkdir -p -m 777 #{shared_path}/log &&
|
42
|
+
mkdir -p -m 777 #{shared_path}/pids
|
42
43
|
CMD
|
43
44
|
end
|
44
45
|
|
@@ -64,7 +65,7 @@ desc <<-DESC
|
|
64
65
|
Update all servers with the latest release of the source code. All this does
|
65
66
|
is do a checkout (as defined by the selected scm module).
|
66
67
|
DESC
|
67
|
-
task :update_code, :
|
68
|
+
task :update_code, :except => { :no_release => true } do
|
68
69
|
on_rollback { delete release_path, :recursive => true }
|
69
70
|
|
70
71
|
source.checkout(self)
|
@@ -74,13 +75,23 @@ task :update_code, :roles => [:app, :db, :web] do
|
|
74
75
|
ln -nfs #{shared_path}/log #{release_path}/log &&
|
75
76
|
ln -nfs #{shared_path}/system #{release_path}/public/system
|
76
77
|
CMD
|
78
|
+
|
79
|
+
run <<-CMD
|
80
|
+
test -d #{shared_path}/pids &&
|
81
|
+
rm -rf #{release_path}/tmp/pids &&
|
82
|
+
ln -nfs #{shared_path}/pids #{release_path}/tmp/pids; true
|
83
|
+
CMD
|
84
|
+
|
85
|
+
# uncache the list of releases, so that the next time it is called it will
|
86
|
+
# include the newly released path.
|
87
|
+
@releases = nil
|
77
88
|
end
|
78
89
|
|
79
90
|
desc <<-DESC
|
80
91
|
Rollback the latest checked-out version to the previous one by fixing the
|
81
92
|
symlinks and deleting the current release from all servers.
|
82
93
|
DESC
|
83
|
-
task :rollback_code, :
|
94
|
+
task :rollback_code, :except => { :no_release => true } do
|
84
95
|
if releases.length < 2
|
85
96
|
raise "could not rollback the code because there is no prior release"
|
86
97
|
else
|
@@ -95,7 +106,7 @@ desc <<-DESC
|
|
95
106
|
Update the 'current' symlink to point to the latest version of
|
96
107
|
the application's code.
|
97
108
|
DESC
|
98
|
-
task :symlink, :
|
109
|
+
task :symlink, :except => { :no_release => true } do
|
99
110
|
on_rollback { run "ln -nfs #{previous_release} #{current_path}" }
|
100
111
|
run "ln -nfs #{current_release} #{current_path}"
|
101
112
|
end
|
@@ -109,6 +120,16 @@ task :restart, :roles => :app do
|
|
109
120
|
send(run_method, "#{current_path}/script/process/reaper")
|
110
121
|
end
|
111
122
|
|
123
|
+
desc <<-DESC
|
124
|
+
Updates the code and fixes the symlink under a transaction
|
125
|
+
DESC
|
126
|
+
task :update do
|
127
|
+
transaction do
|
128
|
+
update_code
|
129
|
+
symlink
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
112
133
|
desc <<-DESC
|
113
134
|
Run the migrate rake task. By default, it runs this in the version of the app
|
114
135
|
indicated by the 'current' symlink. (This means you should not invoke this task
|
@@ -137,11 +158,7 @@ A macro-task that updates the code, fixes the symlink, and restarts the
|
|
137
158
|
application servers.
|
138
159
|
DESC
|
139
160
|
task :deploy do
|
140
|
-
|
141
|
-
update_code
|
142
|
-
symlink
|
143
|
-
end
|
144
|
-
|
161
|
+
update
|
145
162
|
restart
|
146
163
|
end
|
147
164
|
|
@@ -240,3 +257,14 @@ task :invoke, :roles => Capistrano.str2roles(ENV["ROLES"] || "") do
|
|
240
257
|
method = ENV["SUDO"] ? :sudo : :run
|
241
258
|
send(method, ENV["COMMAND"])
|
242
259
|
end
|
260
|
+
|
261
|
+
desc <<-DESC
|
262
|
+
Begin an interactive Capistrano session. This gives you an interactive
|
263
|
+
terminal from which to execute tasks and commands on all of your servers.
|
264
|
+
(This is still an experimental feature, and is subject to change without
|
265
|
+
notice!)
|
266
|
+
DESC
|
267
|
+
task(:shell) do
|
268
|
+
require 'capistrano/shell'
|
269
|
+
Capistrano::Shell.run!(self)
|
270
|
+
end
|
data/lib/capistrano/scm/base.rb
CHANGED
@@ -28,7 +28,6 @@ module Capistrano
|
|
28
28
|
private
|
29
29
|
|
30
30
|
def run_checkout(actor, guts, &block)
|
31
|
-
log = "#{configuration.deploy_to}/revisions.log"
|
32
31
|
directory = File.basename(configuration.release_path)
|
33
32
|
|
34
33
|
command = <<-STR
|
@@ -53,7 +52,7 @@ module Capistrano
|
|
53
52
|
def logging_commands(directory = nil)
|
54
53
|
log = "#{configuration.deploy_to}/revisions.log"
|
55
54
|
|
56
|
-
"(test -e #{log} || touch #{log} && chmod 666 #{log}) && " +
|
55
|
+
"(test -e #{log} || (touch #{log} && chmod 666 #{log})) && " +
|
57
56
|
"echo `date +\"%Y-%m-%d %H:%M:%S\"` $USER #{configuration.revision} #{directory} >> #{log};"
|
58
57
|
end
|
59
58
|
end
|
data/lib/capistrano/scm/cvs.rb
CHANGED
@@ -33,6 +33,11 @@ module Capistrano
|
|
33
33
|
class Cvs < Base
|
34
34
|
def initialize(configuration)
|
35
35
|
super(configuration)
|
36
|
+
|
37
|
+
if not @configuration.respond_to?(:local) then
|
38
|
+
@configuration.set(:local,".")
|
39
|
+
end
|
40
|
+
|
36
41
|
if not configuration.respond_to?(:branch) then
|
37
42
|
configuration.set(:branch) { self.current_branch }
|
38
43
|
else
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'capistrano/scm/base'
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
module SCM
|
5
|
+
|
6
|
+
# An SCM module for using Mercurial as your source control tool.
|
7
|
+
# You can use it by placing the following line in your configuration:
|
8
|
+
#
|
9
|
+
# set :scm, :mercurial
|
10
|
+
#
|
11
|
+
# Also, this module accepts a <tt>:mercurial</tt> configuration variable,
|
12
|
+
# which (if specified) will be used as the full path to the hg
|
13
|
+
# executable on the remote machine:
|
14
|
+
#
|
15
|
+
# set :mercurial, "/usr/local/bin/hg"
|
16
|
+
class Mercurial < Base
|
17
|
+
# Return a string identifying the tip changeset in the mercurial
|
18
|
+
# repository. Note that this fetches the tip changeset from the
|
19
|
+
# local repository, but capistrano will deploy from your _remote_
|
20
|
+
# repository. So just make sure your local repository is synchronized
|
21
|
+
# with your remote one.
|
22
|
+
def latest_revision
|
23
|
+
`#{mercurial} tip --template '{node|short}'`
|
24
|
+
end
|
25
|
+
|
26
|
+
# Return the changeset currently deployed.
|
27
|
+
def current_revision(actor)
|
28
|
+
# NOTE:
|
29
|
+
# copied almost verbatim from svn except its not cast into an int
|
30
|
+
# this should be the same for almost _every_ scm can we take it out
|
31
|
+
# of SCM-specific code?
|
32
|
+
latest = actor.releases.last
|
33
|
+
grep = %(grep " #{latest}$" #{configuration.deploy_to}/revisions.log)
|
34
|
+
result = ""
|
35
|
+
actor.run(grep, :once => true) do |ch, str, out|
|
36
|
+
result << out if str == :out
|
37
|
+
raise "could not determine current changeset" if str == :err
|
38
|
+
end
|
39
|
+
|
40
|
+
date, time, user, changeset, dir = result.split
|
41
|
+
raise "current changeset not found in revisions.log" unless dir == latest
|
42
|
+
changeset
|
43
|
+
end
|
44
|
+
|
45
|
+
# Return a string containing the diff between the two changesets. +from+
|
46
|
+
# and +to+ may be in any format that mercurial recognizes as a valid
|
47
|
+
# changeset. If +from+ is +nil+, it defaults to the last deployed
|
48
|
+
# changeset. If +to+ is +nil+, it defaults to the current working
|
49
|
+
# directory.
|
50
|
+
def diff(actor, from = current_revision(actor), to = nil)
|
51
|
+
cmd = "#{mercurial} diff -r #{from}"
|
52
|
+
cmd << " -r #{to}" if to
|
53
|
+
`#{cmd}`
|
54
|
+
end
|
55
|
+
|
56
|
+
# Check out (on all servers associated with the current task) the latest
|
57
|
+
# revision. Uses the given actor instance to execute the command. If
|
58
|
+
# mercurial asks for a password this will automatically provide it
|
59
|
+
# (assuming the requested password is the same as the password for
|
60
|
+
# logging into the remote server.) If ssh repository method is used,
|
61
|
+
# authorized keys must be setup.
|
62
|
+
def checkout(actor)
|
63
|
+
command = "#{mercurial} clone -U #{configuration.repository} " +
|
64
|
+
"#{actor.release_path} && " +
|
65
|
+
"#{mercurial} -R #{actor.release_path} update " +
|
66
|
+
"-C #{configuration.revision} &&"
|
67
|
+
run_checkout(actor, command, &hg_stream_handler(actor))
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
def mercurial
|
72
|
+
configuration[:mercurial] || "hg"
|
73
|
+
end
|
74
|
+
|
75
|
+
def hg_stream_handler(actor)
|
76
|
+
Proc.new do |ch, stream, out|
|
77
|
+
prefix = "#{stream} :: #{ch[:host]}"
|
78
|
+
actor.logger.info out, prefix
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -16,22 +16,14 @@ module Capistrano
|
|
16
16
|
# set :svn, "/opt/local/bin/svn"
|
17
17
|
class Subversion < Base
|
18
18
|
# Return an integer identifying the last known revision in the svn
|
19
|
-
# repository. (This integer is currently the revision number.)
|
20
|
-
# revision does not exist in the given repository, this routine will
|
21
|
-
# walk up the directory tree until it finds it.
|
19
|
+
# repository. (This integer is currently the revision number.)
|
22
20
|
def latest_revision
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
if @latest_revision.nil?
|
29
|
-
# if a revision number was not reported, move up a level in the path
|
30
|
-
# and try again.
|
31
|
-
repo = File.dirname(repo)
|
21
|
+
@latest_revision ||= begin
|
22
|
+
configuration.logger.debug "querying latest revision..."
|
23
|
+
match = svn_log(configuration.repository).scan(/r(\d+)/).first or
|
24
|
+
raise "Could not determine latest revision"
|
25
|
+
match.first
|
32
26
|
end
|
33
|
-
end
|
34
|
-
@latest_revision
|
35
27
|
end
|
36
28
|
|
37
29
|
# Return the number of the revision currently deployed.
|
@@ -87,13 +79,17 @@ module Capistrano
|
|
87
79
|
end
|
88
80
|
|
89
81
|
def svn_log(path)
|
90
|
-
`svn log -q
|
82
|
+
`svn log -q --limit 1 #{path}`
|
91
83
|
end
|
92
84
|
|
93
85
|
def svn_password
|
94
86
|
configuration[:svn_password] || configuration[:password]
|
95
87
|
end
|
96
88
|
|
89
|
+
def svn_passphrase
|
90
|
+
configuration[:svn_passphrase] || svn_password
|
91
|
+
end
|
92
|
+
|
97
93
|
def svn_stream_handler(actor)
|
98
94
|
Proc.new do |ch, stream, out|
|
99
95
|
prefix = "#{stream} :: #{ch[:host]}"
|
@@ -105,14 +101,18 @@ module Capistrano
|
|
105
101
|
actor.logger.info "subversion is asking whether to connect or not",
|
106
102
|
prefix
|
107
103
|
ch.send_data "yes\n"
|
108
|
-
elsif out =~ %r{passphrase}
|
109
|
-
message = "subversion needs your key's passphrase
|
104
|
+
elsif out =~ %r{passphrase}i
|
105
|
+
message = "subversion needs your key's passphrase"
|
110
106
|
actor.logger.info message, prefix
|
111
|
-
ch.send_data "\n"
|
107
|
+
ch.send_data "#{svn_passphrase}\n"
|
112
108
|
elsif out =~ %r{The entry \'(\w+)\' is no longer a directory}
|
113
109
|
message = "subversion can't update because directory '#{$1}' was replaced. Please add it to svn:ignore."
|
114
110
|
actor.logger.info message, prefix
|
115
111
|
raise message
|
112
|
+
elsif out =~ %r{accept \(t\)emporarily}
|
113
|
+
message = "accepting certificate temporarily"
|
114
|
+
actor.logger.info message, prefix
|
115
|
+
ch.send_data "t\n"
|
116
116
|
end
|
117
117
|
end
|
118
118
|
end
|