capistrano 1.2.0 → 1.3.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 +31 -1
- data/lib/capistrano/actor.rb +4 -3
- data/lib/capistrano/cli.rb +43 -6
- data/lib/capistrano/command.rb +3 -1
- data/lib/capistrano/configuration.rb +0 -2
- data/lib/capistrano/gateway.rb +6 -4
- data/lib/capistrano/generators/rails/deployment/templates/capistrano.rake +3 -0
- data/lib/capistrano/recipes/standard.rb +11 -3
- data/lib/capistrano/scm/cvs.rb +1 -1
- data/lib/capistrano/ssh.rb +24 -1
- data/lib/capistrano/version.rb +1 -1
- data/test/actor_test.rb +9 -0
- data/test/ssh_test.rb +80 -0
- metadata +2 -2
data/CHANGELOG
CHANGED
@@ -1,4 +1,34 @@
|
|
1
|
-
*
|
1
|
+
*1.3.0* (December 23, 2006)
|
2
|
+
|
3
|
+
* Deprecate rake integration in favor of invoking `cap' directly [Jamis Buck]
|
4
|
+
|
5
|
+
* Make sure the CVS module references the repository explicitly in cvs_log [weyus@att.net]
|
6
|
+
|
7
|
+
* Remove trace messages when loading a file [Jamis Buck]
|
8
|
+
|
9
|
+
* Cleaner error messages for authentication failures and command errors [Jamis Buck]
|
10
|
+
|
11
|
+
* Added support for ~/.caprc, also -x and -c switches. [Jamis Buck]
|
12
|
+
|
13
|
+
* Updated migrate action to use db:migrate task in Rails instead of the deprecated migrate task [DHH]
|
14
|
+
|
15
|
+
* Allow SSH user and port to be encoded in the hostname strings [Ezra Zygmuntowicz]
|
16
|
+
|
17
|
+
* Fixed that new checkouts were not group-writable [DHH, Jamis Buck]
|
18
|
+
|
19
|
+
* Fixed that cap setup would use 755 on the deploy_to and shared directory roots instead of 775 [DHH]
|
20
|
+
|
21
|
+
* Don't run the cleanup task on servers marked no_release [Jamis Buck]
|
22
|
+
|
23
|
+
* Fix typo in default_io_proc so it correctly checks the stream parameter to see if it is the error stream [Stephen Haberman]
|
24
|
+
|
25
|
+
* Make sure assets in images, javascripts, and stylesheets are touched after updating the code, to ensure the asset timestamping feature of rails works correctly [Jamis Buck]
|
26
|
+
|
27
|
+
* Added warning if password is prompted for and termios is not installed [John Labovitz]
|
28
|
+
|
29
|
+
* Added :as option to sudo, so you can specify who the command is executed as [Mark Imbriaco]
|
30
|
+
|
31
|
+
*1.2.0* (September 14, 2006)
|
2
32
|
|
3
33
|
* Add experimental 'shell' task [Jamis Buck]
|
4
34
|
|
data/lib/capistrano/actor.rb
CHANGED
@@ -38,7 +38,7 @@ module Capistrano
|
|
38
38
|
self.transfer_factory = Transfer
|
39
39
|
|
40
40
|
self.default_io_proc = Proc.new do |ch, stream, out|
|
41
|
-
level =
|
41
|
+
level = stream == :err ? :important : :info
|
42
42
|
ch[:actor].logger.send(level, out, "#{stream} :: #{ch[:host]}")
|
43
43
|
end
|
44
44
|
|
@@ -277,9 +277,10 @@ module Capistrano
|
|
277
277
|
# in order to prevent _each host_ from prompting when the password was
|
278
278
|
# wrong, let's track which host prompted first and only allow subsequent
|
279
279
|
# prompts from that host.
|
280
|
-
prompt_host = nil
|
280
|
+
prompt_host = nil
|
281
|
+
user = options[:as].nil? ? '' : "-u #{options[:as]}"
|
281
282
|
|
282
|
-
run "#{sudo_command} #{command}", options do |ch, stream, out|
|
283
|
+
run "#{sudo_command} #{user} #{command}", options do |ch, stream, out|
|
283
284
|
if out =~ /^Password:/
|
284
285
|
ch.send_data "#{password}\n"
|
285
286
|
elsif out =~ /try again/
|
data/lib/capistrano/cli.rb
CHANGED
@@ -16,11 +16,7 @@ module Capistrano
|
|
16
16
|
# This requires the termios library to be installed (which, unfortunately,
|
17
17
|
# is not available for Windows).
|
18
18
|
begin
|
19
|
-
|
20
|
-
require 'termios'
|
21
|
-
else
|
22
|
-
raise LoadError
|
23
|
-
end
|
19
|
+
require 'termios'
|
24
20
|
|
25
21
|
# Enable or disable stdin echoing to the terminal.
|
26
22
|
def self.echo(enable)
|
@@ -43,6 +39,10 @@ module Capistrano
|
|
43
39
|
# if termios is not available, echo suppression will not be available
|
44
40
|
# either.
|
45
41
|
def self.with_echo
|
42
|
+
unless @warned_about_echo
|
43
|
+
puts "WARNING: Password will echo -- install the 'termios' gem to hide your password." if !defined?(Termios) && RUBY_PLATFORM !~ /mswin/
|
44
|
+
@warned_about_echo = true
|
45
|
+
end
|
46
46
|
echo(false)
|
47
47
|
yield
|
48
48
|
ensure
|
@@ -96,7 +96,7 @@ module Capistrano
|
|
96
96
|
def initialize(args = ARGV)
|
97
97
|
@args = args
|
98
98
|
@options = { :recipes => [], :actions => [], :vars => {},
|
99
|
-
:pre_vars => {} }
|
99
|
+
:pre_vars => {}, :dotfile => default_dotfile }
|
100
100
|
|
101
101
|
OptionParser.new do |opts|
|
102
102
|
opts.banner = "Usage: #{$0} [options] [args]"
|
@@ -110,6 +110,14 @@ module Capistrano
|
|
110
110
|
"be specified, and are loaded in the given order."
|
111
111
|
) { |value| @options[:actions] << value }
|
112
112
|
|
113
|
+
opts.on("-c", "--caprc FILE",
|
114
|
+
"Specify an alternate personal config file to load.",
|
115
|
+
"(Default: #{@options[:dotfile]})"
|
116
|
+
) do |value|
|
117
|
+
abort "The config file `#{value}' does not exist" unless File.exist?(value)
|
118
|
+
@options[:dotfile] = value
|
119
|
+
end
|
120
|
+
|
113
121
|
opts.on("-f", "--file FILE",
|
114
122
|
"A recipe file to load. Multiple recipes may",
|
115
123
|
"be specified, and are loaded in the given order."
|
@@ -147,6 +155,12 @@ module Capistrano
|
|
147
155
|
@options[:pre_vars][name.to_sym] = value
|
148
156
|
end
|
149
157
|
|
158
|
+
opts.on("-x", "--skip-config",
|
159
|
+
"Disables the loading of the default personal config",
|
160
|
+
"file. Specifying -C after this option will reenable",
|
161
|
+
"it. (Default: config file is loaded)"
|
162
|
+
) { @options[:dotfile] = nil }
|
163
|
+
|
150
164
|
opts.separator ""
|
151
165
|
opts.separator "Framework Integration Options --------"
|
152
166
|
opts.separator ""
|
@@ -245,6 +259,7 @@ DETAIL
|
|
245
259
|
config.set :pretend, options[:pretend]
|
246
260
|
|
247
261
|
options[:pre_vars].each { |name, value| config.set(name, value) }
|
262
|
+
config.load(@options[:dotfile]) if @options[:dotfile] && File.exist?(@options[:dotfile])
|
248
263
|
|
249
264
|
# load the standard recipe definition
|
250
265
|
config.load "standard"
|
@@ -254,6 +269,8 @@ DETAIL
|
|
254
269
|
|
255
270
|
actor = config.actor
|
256
271
|
options[:actions].each { |action| actor.send action }
|
272
|
+
rescue Exception => error
|
273
|
+
handle_error(error)
|
257
274
|
end
|
258
275
|
|
259
276
|
# Load the Rails generator and apply it to the specified directory.
|
@@ -288,6 +305,16 @@ DETAIL
|
|
288
305
|
end
|
289
306
|
end
|
290
307
|
|
308
|
+
def default_dotfile
|
309
|
+
File.join(home_directory, ".caprc")
|
310
|
+
end
|
311
|
+
|
312
|
+
def home_directory
|
313
|
+
ENV["HOME"] ||
|
314
|
+
(ENV["HOMEPATH"] && "#{ENV["HOMEDRIVE"]}#{ENV["HOMEPATH"]}") ||
|
315
|
+
"/"
|
316
|
+
end
|
317
|
+
|
291
318
|
def look_for_default_recipe_file!
|
292
319
|
DEFAULT_RECIPES.each do |file|
|
293
320
|
if File.exist?(file)
|
@@ -300,5 +327,15 @@ DETAIL
|
|
300
327
|
def look_for_raw_actions!
|
301
328
|
@options[:actions].concat(@args)
|
302
329
|
end
|
330
|
+
|
331
|
+
def handle_error(error)
|
332
|
+
case error
|
333
|
+
when Net::SSH::AuthenticationFailed
|
334
|
+
abort "authentication failed for `#{error.message}'"
|
335
|
+
when Capistrano::Command::Error
|
336
|
+
abort(error.message)
|
337
|
+
else raise error
|
338
|
+
end
|
339
|
+
end
|
303
340
|
end
|
304
341
|
end
|
data/lib/capistrano/command.rb
CHANGED
@@ -3,6 +3,8 @@ module Capistrano
|
|
3
3
|
# This class encapsulates a single command to be executed on a set of remote
|
4
4
|
# machines, in parallel.
|
5
5
|
class Command
|
6
|
+
class Error < RuntimeError; end
|
7
|
+
|
6
8
|
attr_reader :servers, :command, :options, :actor
|
7
9
|
|
8
10
|
def initialize(servers, command, callback, options, actor) #:nodoc:
|
@@ -42,7 +44,7 @@ module Capistrano
|
|
42
44
|
logger.trace "command finished"
|
43
45
|
|
44
46
|
if failed = @channels.detect { |ch| ch[:status] != 0 }
|
45
|
-
raise "command #{@command.inspect} failed on #{failed[:host]}"
|
47
|
+
raise Error, "command #{@command.inspect} failed on #{failed[:host]}"
|
46
48
|
end
|
47
49
|
|
48
50
|
self
|
@@ -146,11 +146,9 @@ module Capistrano
|
|
146
146
|
load :string => File.read(file), :name => options[:name] || file
|
147
147
|
|
148
148
|
elsif options[:string]
|
149
|
-
logger.trace "loading configuration #{options[:name] || "<eval>"}"
|
150
149
|
instance_eval(options[:string], options[:name] || "<eval>")
|
151
150
|
|
152
151
|
elsif options[:proc]
|
153
|
-
logger.trace "loading configuration #{eval("__FILE__", options[:proc])}"
|
154
152
|
instance_eval(&options[:proc])
|
155
153
|
|
156
154
|
else
|
data/lib/capistrano/gateway.rb
CHANGED
@@ -30,7 +30,6 @@ module Capistrano
|
|
30
30
|
|
31
31
|
def initialize(server, config) #:nodoc:
|
32
32
|
@config = config
|
33
|
-
@pending_forward_requests = {}
|
34
33
|
@next_port = MAX_PORT
|
35
34
|
@terminate_thread = false
|
36
35
|
@port_guard = Mutex.new
|
@@ -70,12 +69,15 @@ module Capistrano
|
|
70
69
|
def connect_to(server)
|
71
70
|
connection = nil
|
72
71
|
@config.logger.trace "establishing connection to #{server} via gateway"
|
73
|
-
|
72
|
+
local_port = next_port
|
74
73
|
|
75
74
|
thread = Thread.new do
|
76
75
|
begin
|
77
|
-
|
78
|
-
|
76
|
+
user, server_stripped, port = SSH.parse_server(server)
|
77
|
+
@config.ssh_options[:username] = user if user
|
78
|
+
remote_port = port || 22
|
79
|
+
@session.forward.local(local_port, server_stripped, remote_port)
|
80
|
+
connection = SSH.connect('127.0.0.1', @config, local_port)
|
79
81
|
@config.logger.trace "connection to #{server} via gateway established"
|
80
82
|
rescue Errno::EADDRINUSE
|
81
83
|
port = next_port
|
@@ -12,6 +12,9 @@ def cap(*parameters)
|
|
12
12
|
|
13
13
|
require 'capistrano/cli'
|
14
14
|
|
15
|
+
STDERR.puts "Capistrano/Rake integration is deprecated."
|
16
|
+
STDERR.puts "Please invoke the 'cap' command directly: `cap #{parameters.join(" ")}'"
|
17
|
+
|
15
18
|
Capistrano::CLI.new(parameters.map { |param| param.to_s }).execute!
|
16
19
|
end
|
17
20
|
|
@@ -37,7 +37,7 @@ end
|
|
37
37
|
desc "Set up the expected application directory structure on all boxes"
|
38
38
|
task :setup, :except => { :no_release => true } do
|
39
39
|
run <<-CMD
|
40
|
-
mkdir -p -m 775 #{releases_path} #{shared_path}/system &&
|
40
|
+
mkdir -p -m 775 #{deploy_to} #{releases_path} #{shared_path} #{shared_path}/system &&
|
41
41
|
mkdir -p -m 777 #{shared_path}/log &&
|
42
42
|
mkdir -p -m 777 #{shared_path}/pids
|
43
43
|
CMD
|
@@ -70,6 +70,8 @@ task :update_code, :except => { :no_release => true } do
|
|
70
70
|
|
71
71
|
source.checkout(self)
|
72
72
|
|
73
|
+
run "chmod -R g+w #{release_path}"
|
74
|
+
|
73
75
|
run <<-CMD
|
74
76
|
rm -rf #{release_path}/log #{release_path}/public/system &&
|
75
77
|
ln -nfs #{shared_path}/log #{release_path}/log &&
|
@@ -82,6 +84,12 @@ task :update_code, :except => { :no_release => true } do
|
|
82
84
|
ln -nfs #{shared_path}/pids #{release_path}/tmp/pids; true
|
83
85
|
CMD
|
84
86
|
|
87
|
+
# update the asset timestamps so they are in sync across all servers. This
|
88
|
+
# lets the asset timestamping feature of rails work correctly
|
89
|
+
stamp = Time.now.utc.strftime("%Y%m%d%H%M.%S")
|
90
|
+
asset_paths = %w(images stylesheets javascripts).map { |p| "#{release_path}/public/#{p}" }
|
91
|
+
run "find #{asset_paths.join(" ")} -exec touch -t #{stamp} {} \\;; true"
|
92
|
+
|
85
93
|
# uncache the list of releases, so that the next time it is called it will
|
86
94
|
# include the newly released path.
|
87
95
|
@releases = nil
|
@@ -150,7 +158,7 @@ task :migrate, :roles => :db, :only => { :primary => true } do
|
|
150
158
|
end
|
151
159
|
|
152
160
|
run "cd #{directory} && " +
|
153
|
-
"#{rake} RAILS_ENV=#{rails_env} #{migrate_env} migrate"
|
161
|
+
"#{rake} RAILS_ENV=#{rails_env} #{migrate_env} db:migrate"
|
154
162
|
end
|
155
163
|
|
156
164
|
desc <<-DESC
|
@@ -212,7 +220,7 @@ releases are retained, but this can be configured with the 'keep_releases'
|
|
212
220
|
variable. This will use sudo to do the delete by default, but you can specify
|
213
221
|
that run should be used by setting the :use_sudo variable to false.
|
214
222
|
DESC
|
215
|
-
task :cleanup do
|
223
|
+
task :cleanup, :except => { :no_release => true } do
|
216
224
|
count = (self[:keep_releases] || 5).to_i
|
217
225
|
if count >= releases.length
|
218
226
|
logger.important "no old releases to clean up"
|
data/lib/capistrano/scm/cvs.rb
CHANGED
data/lib/capistrano/ssh.rb
CHANGED
@@ -27,12 +27,35 @@ module Capistrano
|
|
27
27
|
:password => password_value,
|
28
28
|
:port => port,
|
29
29
|
:auth_methods => methods.shift }.merge(config.ssh_options)
|
30
|
-
|
30
|
+
|
31
|
+
user, server_stripped, port = parse_server(server)
|
32
|
+
ssh_options[:username] = user if user
|
33
|
+
ssh_options[:port] = port if port
|
34
|
+
|
35
|
+
Net::SSH.start(server_stripped,ssh_options,&block)
|
31
36
|
rescue Net::SSH::AuthenticationFailed
|
32
37
|
raise if methods.empty?
|
33
38
|
password_value = config.password
|
34
39
|
retry
|
35
40
|
end
|
36
41
|
end
|
42
|
+
|
43
|
+
# This regex is used for its byproducts, the $1-9 match vars.
|
44
|
+
# This regex will always match the ssh hostname and if there
|
45
|
+
# is a username or port they will be matched as well. This
|
46
|
+
# allows us to set the username and ssh port right in the
|
47
|
+
# server string: "username@123.12.123.12:8088"
|
48
|
+
# This remains fully backwards compatible and can still be
|
49
|
+
# intermixed with the old way of doing things. usernames
|
50
|
+
# and ports will be used from the server string if present
|
51
|
+
# but they will fall back to the regular defaults when not
|
52
|
+
# present. Returns and array like:
|
53
|
+
# ['bob', 'demo.server.com', '8088']
|
54
|
+
# will always at least return the server:
|
55
|
+
# [nil, 'demo.server.com', nil]
|
56
|
+
def self.parse_server(server)
|
57
|
+
server =~ /^(?:([^;,:=]+)@|)(.*?)(?::(\d+)|)$/
|
58
|
+
[$1, $2, $3]
|
59
|
+
end
|
37
60
|
end
|
38
61
|
end
|
data/lib/capistrano/version.rb
CHANGED
data/test/actor_test.rb
CHANGED
@@ -316,6 +316,15 @@ class ActorTest < Test::Unit::TestCase
|
|
316
316
|
@actor.foo
|
317
317
|
assert_instance_of GatewayConnectionFactory, @actor.factory
|
318
318
|
end
|
319
|
+
|
320
|
+
def test_establish_connection_uses_gateway_if_specified_with_username_and_port
|
321
|
+
@actor.configuration.gateway = "demo@10.example.com:8088"
|
322
|
+
@actor.define_task :foo, :roles => :db do
|
323
|
+
run "do this"
|
324
|
+
end
|
325
|
+
@actor.foo
|
326
|
+
assert_instance_of GatewayConnectionFactory, @actor.factory
|
327
|
+
end
|
319
328
|
|
320
329
|
def test_run_when_not_pretend
|
321
330
|
@actor.define_task :foo do
|
data/test/ssh_test.rb
CHANGED
@@ -42,6 +42,39 @@ class SSHTest < Test::Unit::TestCase
|
|
42
42
|
MockSSH.invocations.first[1][:auth_methods]
|
43
43
|
assert_nil MockSSH.invocations.first[2]
|
44
44
|
end
|
45
|
+
|
46
|
+
def test_explicit_ssh_ports_in_server_string_no_block
|
47
|
+
Net.const_during(:SSH, MockSSH) do
|
48
|
+
Capistrano::SSH.connect('demo.server.i:8088', @config)
|
49
|
+
end
|
50
|
+
|
51
|
+
assert_equal 1, MockSSH.invocations.length
|
52
|
+
assert_equal 'demo.server.i', MockSSH.invocations.first[0]
|
53
|
+
assert_equal '8088', MockSSH.invocations.first[1][:port]
|
54
|
+
assert_equal 'demo', MockSSH.invocations.first[1][:username]
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_explicit_ssh_username_in_server_string_no_block
|
58
|
+
Net.const_during(:SSH, MockSSH) do
|
59
|
+
Capistrano::SSH.connect('bob@demo.server.i', @config)
|
60
|
+
end
|
61
|
+
|
62
|
+
assert_equal 1, MockSSH.invocations.length
|
63
|
+
assert_equal 'demo.server.i', MockSSH.invocations.first[0]
|
64
|
+
assert_equal 22, MockSSH.invocations.first[1][:port]
|
65
|
+
assert_equal 'bob', MockSSH.invocations.first[1][:username]
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_explicit_ssh_username_and_port_in_server_string_no_block
|
69
|
+
Net.const_during(:SSH, MockSSH) do
|
70
|
+
Capistrano::SSH.connect('bob@demo.server.i:8088', @config)
|
71
|
+
end
|
72
|
+
|
73
|
+
assert_equal 1, MockSSH.invocations.length
|
74
|
+
assert_equal 'demo.server.i', MockSSH.invocations.first[0]
|
75
|
+
assert_equal '8088', MockSSH.invocations.first[1][:port]
|
76
|
+
assert_equal 'bob', MockSSH.invocations.first[1][:username]
|
77
|
+
end
|
45
78
|
|
46
79
|
def test_publickey_auth_succeeds_explicit_port_no_block
|
47
80
|
Net.const_during(:SSH, MockSSH) do
|
@@ -53,6 +86,53 @@ class SSHTest < Test::Unit::TestCase
|
|
53
86
|
assert_nil MockSSH.invocations.first[2]
|
54
87
|
end
|
55
88
|
|
89
|
+
|
90
|
+
def test_explicit_ssh_ports_in_server_string_with_block
|
91
|
+
Net.const_during(:SSH, MockSSH) do
|
92
|
+
Capistrano::SSH.connect('demo.server.i:8088', @config) do |session|
|
93
|
+
end
|
94
|
+
end
|
95
|
+
assert_equal 'demo.server.i', MockSSH.invocations.first[0]
|
96
|
+
assert_equal '8088', MockSSH.invocations.first[1][:port]
|
97
|
+
assert_equal 1, MockSSH.invocations.length
|
98
|
+
assert_instance_of Proc, MockSSH.invocations.first[2]
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_explicit_ssh_username_in_server_string_with_block
|
102
|
+
Net.const_during(:SSH, MockSSH) do
|
103
|
+
Capistrano::SSH.connect('bob@demo.server.i', @config) do |session|
|
104
|
+
end
|
105
|
+
end
|
106
|
+
assert_equal 'demo.server.i', MockSSH.invocations.first[0]
|
107
|
+
assert_equal 22, MockSSH.invocations.first[1][:port]
|
108
|
+
assert_equal 1, MockSSH.invocations.length
|
109
|
+
assert_equal 'bob', MockSSH.invocations.first[1][:username]
|
110
|
+
assert_instance_of Proc, MockSSH.invocations.first[2]
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_explicit_ssh_username_and_port_in_server_string_with_block
|
114
|
+
Net.const_during(:SSH, MockSSH) do
|
115
|
+
Capistrano::SSH.connect('bob@demo.server.i:8088', @config) do |session|
|
116
|
+
end
|
117
|
+
end
|
118
|
+
assert_equal 'demo.server.i', MockSSH.invocations.first[0]
|
119
|
+
assert_equal '8088', MockSSH.invocations.first[1][:port]
|
120
|
+
assert_equal 1, MockSSH.invocations.length
|
121
|
+
assert_equal 'bob', MockSSH.invocations.first[1][:username]
|
122
|
+
assert_instance_of Proc, MockSSH.invocations.first[2]
|
123
|
+
end
|
124
|
+
|
125
|
+
def test_parse_server
|
126
|
+
assert_equal(['bob', 'demo.server.i', '8088'],
|
127
|
+
Capistrano::SSH.parse_server("bob@demo.server.i:8088"))
|
128
|
+
assert_equal([nil, 'demo.server.i', '8088'],
|
129
|
+
Capistrano::SSH.parse_server("demo.server.i:8088"))
|
130
|
+
assert_equal(['bob', 'demo.server.i', nil],
|
131
|
+
Capistrano::SSH.parse_server("bob@demo.server.i"))
|
132
|
+
assert_equal([nil, 'demo.server.i', nil],
|
133
|
+
Capistrano::SSH.parse_server("demo.server.i"))
|
134
|
+
end
|
135
|
+
|
56
136
|
def test_publickey_auth_succeeds_with_block
|
57
137
|
Net.const_during(:SSH, MockSSH) do
|
58
138
|
Capistrano::SSH.connect('demo.server.i', @config) do |session|
|
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.0
|
|
3
3
|
specification_version: 1
|
4
4
|
name: capistrano
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 1.
|
7
|
-
date: 2006-
|
6
|
+
version: 1.3.0
|
7
|
+
date: 2006-12-23 00:00:00 -07:00
|
8
8
|
summary: Capistrano is a framework and utility for executing commands in parallel on multiple remote machines, via SSH. The primary goal is to simplify and automate the deployment of web applications.
|
9
9
|
require_paths:
|
10
10
|
- lib
|