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