capistrano 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/cap +11 -0
- data/examples/sample.rb +113 -0
- data/lib/capistrano.rb +1 -0
- data/lib/capistrano/actor.rb +438 -0
- data/lib/capistrano/cli.rb +295 -0
- data/lib/capistrano/command.rb +90 -0
- data/lib/capistrano/configuration.rb +243 -0
- data/lib/capistrano/extensions.rb +38 -0
- data/lib/capistrano/gateway.rb +118 -0
- data/lib/capistrano/generators/rails/deployment/deployment_generator.rb +25 -0
- data/lib/capistrano/generators/rails/deployment/templates/capistrano.rake +46 -0
- data/lib/capistrano/generators/rails/deployment/templates/deploy.rb +122 -0
- data/lib/capistrano/generators/rails/loader.rb +20 -0
- data/lib/capistrano/logger.rb +59 -0
- data/lib/capistrano/recipes/standard.rb +242 -0
- data/lib/capistrano/recipes/templates/maintenance.rhtml +53 -0
- data/lib/capistrano/scm/base.rb +62 -0
- data/lib/capistrano/scm/baz.rb +118 -0
- data/lib/capistrano/scm/bzr.rb +70 -0
- data/lib/capistrano/scm/cvs.rb +124 -0
- data/lib/capistrano/scm/darcs.rb +27 -0
- data/lib/capistrano/scm/perforce.rb +139 -0
- data/lib/capistrano/scm/subversion.rb +122 -0
- data/lib/capistrano/ssh.rb +39 -0
- data/lib/capistrano/transfer.rb +90 -0
- data/lib/capistrano/utils.rb +26 -0
- data/lib/capistrano/version.rb +30 -0
- data/test/actor_test.rb +294 -0
- data/test/command_test.rb +43 -0
- data/test/configuration_test.rb +233 -0
- data/test/fixtures/config.rb +5 -0
- data/test/fixtures/custom.rb +3 -0
- data/test/scm/cvs_test.rb +186 -0
- data/test/scm/subversion_test.rb +137 -0
- data/test/ssh_test.rb +104 -0
- data/test/utils.rb +50 -0
- metadata +107 -0
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'capistrano/scm/base'
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
module SCM
|
5
|
+
|
6
|
+
# An SCM module for using perforce as your source control tool.
|
7
|
+
# This module can explicitly selected by placing the following line
|
8
|
+
# in your configuration:
|
9
|
+
#
|
10
|
+
# set :scm, :perforce
|
11
|
+
#
|
12
|
+
# Also, this module accepts a <tt>:p4</tt> configuration variable,
|
13
|
+
# which (if specified) will be used as the full path to the p4
|
14
|
+
# executable on the remote machine:
|
15
|
+
#
|
16
|
+
# set :p4, "/usr/local/bin/p4"
|
17
|
+
#
|
18
|
+
# This module accepts another <tt>:p4sync_flags</tt> configuration
|
19
|
+
# variable, which (if specified) can add extra options. This setting
|
20
|
+
# defaults to the value "-f" which forces resynchronization.
|
21
|
+
#
|
22
|
+
# set :p4sync_flags, "-f"
|
23
|
+
#
|
24
|
+
# This module accepts another <tt>:p4client_root</tt> configuration
|
25
|
+
# variable to handle mapping adjustments. Perforce doesn't have the
|
26
|
+
# ability to sync to a specific directory (e.g. the release_path); the
|
27
|
+
# location being synced to is defined by the client-spec. As such, we
|
28
|
+
# sync the client-spec (defined by <tt>p4client</tt> and then copy from
|
29
|
+
# the determined root of the client-spec mappings to the release_path.
|
30
|
+
# In the unlikely event that your client-spec mappings introduces
|
31
|
+
# directory structure above the rails structure, you can override the
|
32
|
+
# may need to specify the directory
|
33
|
+
#
|
34
|
+
# set :p4client_root, "/user/rmcmahon/project1/code"
|
35
|
+
#
|
36
|
+
# Finally, the module accepts a <tt>p4diff2_options</tt> configuration
|
37
|
+
# variable. This can be used to manipulate the output when running a
|
38
|
+
# diff between what is deployed, and another revision. This option
|
39
|
+
# defaults to "-u". Run 'p4 help diff2' for other options.
|
40
|
+
#
|
41
|
+
class Perforce < Base
|
42
|
+
|
43
|
+
def latest_revision
|
44
|
+
configuration.logger.debug "querying latest revision..." unless @latest_revision
|
45
|
+
@latest_revision = `#{local_p4} counter change`.strip
|
46
|
+
@latest_revision
|
47
|
+
end
|
48
|
+
|
49
|
+
# Return the number of the revision currently deployed.
|
50
|
+
def current_revision(actor)
|
51
|
+
latest = actor.releases.last
|
52
|
+
grep = %(grep " #{latest}$" #{configuration.deploy_to}/revisions.log)
|
53
|
+
result = ""
|
54
|
+
actor.run(grep, :once => true) do |ch, str, out|
|
55
|
+
result << out if str == :out
|
56
|
+
raise "could not determine current revision" if str == :err
|
57
|
+
end
|
58
|
+
|
59
|
+
date, time, user, rev, dir = result.split
|
60
|
+
raise "current revision not found in revisions.log" unless dir == latest
|
61
|
+
rev.to_i
|
62
|
+
end
|
63
|
+
|
64
|
+
# Return a string containing the diff between the two revisions. +from+
|
65
|
+
# and +to+ may be in any format that p4 recognizes as a valid revision
|
66
|
+
# identifiers. If +from+ is +nil+, it defaults to the last deployed
|
67
|
+
# revision. If +to+ is +nil+, it defaults to #head.
|
68
|
+
def diff(actor, from=nil, to=nil)
|
69
|
+
from ||= "@#{current_revision(actor)}"
|
70
|
+
to ||= "#head"
|
71
|
+
p4client = configuration[:p4client]
|
72
|
+
p4diff2_options = configuration[:p4diff2_options]||"-u -db"
|
73
|
+
`#{local_p4} diff2 #{p4diff2_options} //#{p4client}/...#{from} //#{p4client}/...#{to}`
|
74
|
+
end
|
75
|
+
|
76
|
+
# Syncronizes (on all servers associated with the current task) the head
|
77
|
+
# revision of the code. Uses the given actor instance to execute the command.
|
78
|
+
#
|
79
|
+
def checkout(actor)
|
80
|
+
p4sync_flags = configuration[:p4sync_flags] || "-f"
|
81
|
+
p4client_root = configuration[:p4client_root] || "`#{remote_p4} client -o | grep ^Root | cut -f2`"
|
82
|
+
command = "#{remote_p4} sync #{p4sync_flags} && cp -rf #{p4client_root} #{actor.release_path};"
|
83
|
+
run_checkout(actor, command, &p4_stream_handler(actor))
|
84
|
+
end
|
85
|
+
|
86
|
+
def update(actor)
|
87
|
+
raise "#{self.class} doesn't support update(actor)"
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def local_p4
|
93
|
+
add_standard_p4_options('p4')
|
94
|
+
end
|
95
|
+
|
96
|
+
def remote_p4
|
97
|
+
p4_cmd = configuration[:p4] || 'p4'
|
98
|
+
add_standard_p4_options(p4_cmd)
|
99
|
+
end
|
100
|
+
|
101
|
+
def add_standard_p4_options(p4_location)
|
102
|
+
check_settings
|
103
|
+
p4_cmd = p4_location
|
104
|
+
p4_cmd = "#{p4_cmd} -p #{configuration[:p4port]}" if configuration[:p4port]
|
105
|
+
p4_cmd = "#{p4_cmd} -u #{configuration[:p4user]}" if configuration[:p4user]
|
106
|
+
p4_cmd = "#{p4_cmd} -P #{configuration[:p4passwd]}" if configuration[:p4passwd]
|
107
|
+
p4_cmd = "#{p4_cmd} -c #{configuration[:p4client]}" if configuration[:p4client]
|
108
|
+
p4_cmd
|
109
|
+
end
|
110
|
+
|
111
|
+
def check_settings
|
112
|
+
check_setting(:p4port, "Add set :p4port, <your perforce server details e.g. my.p4.server:1666> to deploy.rb")
|
113
|
+
check_setting(:p4user, "Add set :p4user, <your production build username> to deploy.rb")
|
114
|
+
check_setting(:p4passwd, "Add set :p4passwd, <your build user password> to deploy.rb")
|
115
|
+
check_setting(:p4client, "Add set :p4client, <your client-spec name> to deploy.rb")
|
116
|
+
end
|
117
|
+
|
118
|
+
def check_setting(p4setting, message)
|
119
|
+
raise "#{p4setting} is not configured. #{message}" unless configuration[p4setting]
|
120
|
+
end
|
121
|
+
|
122
|
+
def p4_stream_handler(actor)
|
123
|
+
Proc.new do |ch, stream, out|
|
124
|
+
prefix = "#{stream} :: #{ch[:host]}"
|
125
|
+
actor.logger.info out, prefix
|
126
|
+
if out =~ /\(P4PASSWD\) invalid or unset\./i
|
127
|
+
raise "p4passwd is incorrect or unset"
|
128
|
+
elsif out =~ /Can.t create a new user.*/i
|
129
|
+
raise "p4user is incorrect or unset"
|
130
|
+
elsif out =~ /Perforce client error\:/i
|
131
|
+
raise "p4port is incorrect or unset"
|
132
|
+
elsif out =~ /Client \'[\w\-\_\.]+\' unknown.*/i
|
133
|
+
raise "p4client is incorrect or unset"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'capistrano/scm/base'
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
module SCM
|
5
|
+
|
6
|
+
# An SCM module for using subversion as your source control tool. This
|
7
|
+
# module is used by default, but you can explicitly specify it by
|
8
|
+
# placing the following line in your configuration:
|
9
|
+
#
|
10
|
+
# set :scm, :subversion
|
11
|
+
#
|
12
|
+
# Also, this module accepts a <tt>:svn</tt> configuration variable,
|
13
|
+
# which (if specified) will be used as the full path to the svn
|
14
|
+
# executable on the remote machine:
|
15
|
+
#
|
16
|
+
# set :svn, "/opt/local/bin/svn"
|
17
|
+
class Subversion < Base
|
18
|
+
# Return an integer identifying the last known revision in the svn
|
19
|
+
# repository. (This integer is currently the revision number.) If latest
|
20
|
+
# revision does not exist in the given repository, this routine will
|
21
|
+
# walk up the directory tree until it finds it.
|
22
|
+
def latest_revision
|
23
|
+
configuration.logger.debug "querying latest revision..." unless @latest_revision
|
24
|
+
repo = configuration.repository
|
25
|
+
until @latest_revision
|
26
|
+
match = svn_log(repo).scan(/r(\d+)/).first
|
27
|
+
@latest_revision = match ? match.first : nil
|
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)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
@latest_revision
|
35
|
+
end
|
36
|
+
|
37
|
+
# Return the number of the revision currently deployed.
|
38
|
+
def current_revision(actor)
|
39
|
+
latest = actor.releases.last
|
40
|
+
grep = %(grep " #{latest}$" #{configuration.deploy_to}/revisions.log)
|
41
|
+
result = ""
|
42
|
+
actor.run(grep, :once => true) do |ch, str, out|
|
43
|
+
result << out if str == :out
|
44
|
+
raise "could not determine current revision" if str == :err
|
45
|
+
end
|
46
|
+
|
47
|
+
date, time, user, rev, dir = result.split
|
48
|
+
raise "current revision not found in revisions.log" unless dir == latest
|
49
|
+
|
50
|
+
rev.to_i
|
51
|
+
end
|
52
|
+
|
53
|
+
# Return a string containing the diff between the two revisions. +from+
|
54
|
+
# and +to+ may be in any format that svn recognizes as a valid revision
|
55
|
+
# identifier. If +from+ is +nil+, it defaults to the last deployed
|
56
|
+
# revision. If +to+ is +nil+, it defaults to HEAD.
|
57
|
+
def diff(actor, from=nil, to=nil)
|
58
|
+
from ||= current_revision(actor)
|
59
|
+
to ||= "HEAD"
|
60
|
+
|
61
|
+
`svn diff #{configuration.repository}@#{from} #{configuration.repository}@#{to}`
|
62
|
+
end
|
63
|
+
|
64
|
+
# Check out (on all servers associated with the current task) the latest
|
65
|
+
# revision. Uses the given actor instance to execute the command. If
|
66
|
+
# svn asks for a password this will automatically provide it (assuming
|
67
|
+
# the requested password is the same as the password for logging into the
|
68
|
+
# remote server.)
|
69
|
+
def checkout(actor)
|
70
|
+
op = configuration[:checkout] || "co"
|
71
|
+
username = configuration[:svn_username] ? "--username #{configuration[:svn_username]}" : ""
|
72
|
+
command = "#{svn} #{op} #{username} -q -r#{configuration.revision} #{configuration.repository} #{actor.release_path} &&"
|
73
|
+
run_checkout(actor, command, &svn_stream_handler(actor))
|
74
|
+
end
|
75
|
+
|
76
|
+
# Update the current release in-place. This assumes that the original
|
77
|
+
# deployment was made using checkout, and not something like export.
|
78
|
+
def update(actor)
|
79
|
+
command = "cd #{actor.current_path} && #{svn} up -q &&"
|
80
|
+
run_update(actor, command, &svn_stream_handler(actor))
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def svn
|
86
|
+
configuration[:svn] || "svn"
|
87
|
+
end
|
88
|
+
|
89
|
+
def svn_log(path)
|
90
|
+
`svn log -q -rhead #{path}`
|
91
|
+
end
|
92
|
+
|
93
|
+
def svn_password
|
94
|
+
configuration[:svn_password] || configuration[:password]
|
95
|
+
end
|
96
|
+
|
97
|
+
def svn_stream_handler(actor)
|
98
|
+
Proc.new do |ch, stream, out|
|
99
|
+
prefix = "#{stream} :: #{ch[:host]}"
|
100
|
+
actor.logger.info out, prefix
|
101
|
+
if out =~ /\bpassword.*:/i
|
102
|
+
actor.logger.info "subversion is asking for a password", prefix
|
103
|
+
ch.send_data "#{svn_password}\n"
|
104
|
+
elsif out =~ %r{\(yes/no\)}
|
105
|
+
actor.logger.info "subversion is asking whether to connect or not",
|
106
|
+
prefix
|
107
|
+
ch.send_data "yes\n"
|
108
|
+
elsif out =~ %r{passphrase}
|
109
|
+
message = "subversion needs your key's passphrase, sending empty string"
|
110
|
+
actor.logger.info message, prefix
|
111
|
+
ch.send_data "\n"
|
112
|
+
elsif out =~ %r{The entry \'(\w+)\' is no longer a directory}
|
113
|
+
message = "subversion can't update because directory '#{$1}' was replaced. Please add it to svn:ignore."
|
114
|
+
actor.logger.info message, prefix
|
115
|
+
raise message
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
unless ENV['SKIP_VERSION_CHECK']
|
5
|
+
require 'capistrano/version'
|
6
|
+
require 'net/ssh/version'
|
7
|
+
ssh_version = [Net::SSH::Version::MAJOR, Net::SSH::Version::MINOR, Net::SSH::Version::TINY]
|
8
|
+
required_version = [1,0,5]
|
9
|
+
if !Version.check(required_version, ssh_version)
|
10
|
+
raise "You have Net::SSH #{ssh_version.join(".")}, but you need at least #{required_version.join(".")}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# A helper class for dealing with SSH connections.
|
15
|
+
class SSH
|
16
|
+
# An abstraction to make it possible to connect to the server via public key
|
17
|
+
# without prompting for the password. If the public key authentication fails
|
18
|
+
# this will fall back to password authentication.
|
19
|
+
#
|
20
|
+
# If a block is given, the new session is yielded to it, otherwise the new
|
21
|
+
# session is returned.
|
22
|
+
def self.connect(server, config, port=22, &block)
|
23
|
+
methods = [ %w(publickey hostbased), %w(password keyboard-interactive) ]
|
24
|
+
password_value = nil
|
25
|
+
|
26
|
+
begin
|
27
|
+
ssh_options = { :username => config.user,
|
28
|
+
:password => password_value,
|
29
|
+
:port => port,
|
30
|
+
:auth_methods => methods.shift }.merge(config.ssh_options)
|
31
|
+
Net::SSH.start(server,ssh_options,&block)
|
32
|
+
rescue Net::SSH::AuthenticationFailed
|
33
|
+
raise if methods.empty?
|
34
|
+
password_value = config.password
|
35
|
+
retry
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
begin
|
2
|
+
require 'capistrano/version'
|
3
|
+
require 'net/sftp'
|
4
|
+
require 'net/sftp/version'
|
5
|
+
sftp_version = [Net::SFTP::Version::MAJOR, Net::SFTP::Version::MINOR, Net::SFTP::Version::TINY]
|
6
|
+
required_version = [1,1,0]
|
7
|
+
if !Capistrano::Version.check(required_version, sftp_version)
|
8
|
+
warn "You have Net::SFTP #{sftp_version.join(".")}, but you need at least #{required_version.join(".")}. Net::SFTP will not be used."
|
9
|
+
Capistrano::SFTP = false
|
10
|
+
else
|
11
|
+
Capistrano::SFTP = true
|
12
|
+
end
|
13
|
+
rescue LoadError
|
14
|
+
Capistrano::SFTP = false
|
15
|
+
end
|
16
|
+
|
17
|
+
module Capistrano
|
18
|
+
|
19
|
+
# This class encapsulates a single file transfer to be performed in parallel
|
20
|
+
# across multiple machines, using the SFTP protocol.
|
21
|
+
class Transfer
|
22
|
+
def initialize(servers, actor, filename, params={}) #:nodoc:
|
23
|
+
@servers = servers
|
24
|
+
@actor = actor
|
25
|
+
@filename = filename
|
26
|
+
@params = params
|
27
|
+
@completed = 0
|
28
|
+
@failed = 0
|
29
|
+
@sftps = setup_transfer
|
30
|
+
end
|
31
|
+
|
32
|
+
def logger #:nodoc:
|
33
|
+
@actor.logger
|
34
|
+
end
|
35
|
+
|
36
|
+
# Uploads to all specified servers in parallel.
|
37
|
+
def process!
|
38
|
+
logger.debug "uploading #{@filename}"
|
39
|
+
|
40
|
+
loop do
|
41
|
+
@sftps.each { |sftp| sftp.channel.connection.process(true) }
|
42
|
+
break if @completed == @servers.length
|
43
|
+
end
|
44
|
+
|
45
|
+
logger.trace "upload finished"
|
46
|
+
|
47
|
+
if @failed > 0
|
48
|
+
raise "upload of #{@filename} failed on one or more hosts"
|
49
|
+
end
|
50
|
+
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def setup_transfer
|
57
|
+
@servers.map do |server|
|
58
|
+
sftp = @actor.sessions[server].sftp
|
59
|
+
sftp.connect unless sftp.state == :open
|
60
|
+
|
61
|
+
sftp.open(@filename, IO::WRONLY | IO::CREAT | IO::TRUNC, @params[:mode] || 0660) do |status, handle|
|
62
|
+
break unless check_status("open #{@filename}", server, status)
|
63
|
+
|
64
|
+
logger.info "uploading data to #{server}:#{@filename}"
|
65
|
+
sftp.write(handle, @params[:data] || "") do |status|
|
66
|
+
break unless check_status("write to #{server}:#{@filename}", server, status)
|
67
|
+
sftp.close_handle(handle) do
|
68
|
+
logger.debug "done uploading data to #{server}:#{@filename}"
|
69
|
+
@completed += 1
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
sftp
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def check_status(action, server, status)
|
79
|
+
if status.code != Net::SFTP::Session::FX_OK
|
80
|
+
logger.error "could not #{action} on #{server} (#{status.message})"
|
81
|
+
@failed += 1
|
82
|
+
@completed += 1
|
83
|
+
return false
|
84
|
+
end
|
85
|
+
|
86
|
+
true
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Capistrano
|
2
|
+
# A helper method for converting a comma-delimited string into an array of
|
3
|
+
# roles.
|
4
|
+
def self.str2roles(string)
|
5
|
+
list = string.split(/,/).map { |s| s.strip.to_sym }
|
6
|
+
list.empty? ? nil : list
|
7
|
+
end
|
8
|
+
|
9
|
+
# Used by third-party task bundles to identify the capistrano configuration
|
10
|
+
# that is loading them. It's return value is not reliable in other contexts.
|
11
|
+
# If +require_config+ is not false, an exception will be raised if the current
|
12
|
+
# configuration is not set.
|
13
|
+
def self.configuration(require_config=false)
|
14
|
+
config = Thread.current[:capistrano_configuration]
|
15
|
+
if require_config && config.nil?
|
16
|
+
raise "Please require this file from within a Capistrano recipe"
|
17
|
+
end
|
18
|
+
config
|
19
|
+
end
|
20
|
+
|
21
|
+
# Used internally by Capistrano to specify the current configuration before
|
22
|
+
# loading a third-party task bundle.
|
23
|
+
def self.configuration=(config)
|
24
|
+
Thread.current[:capistrano_configuration] = config
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Capistrano
|
2
|
+
module Version #:nodoc:
|
3
|
+
# A method for comparing versions of required modules. It expects two
|
4
|
+
# arrays as parameters, and returns true if the first is no more than the
|
5
|
+
# second.
|
6
|
+
def self.check(expected, actual) #:nodoc:
|
7
|
+
good = false
|
8
|
+
if actual[0] > expected[0]
|
9
|
+
good = true
|
10
|
+
elsif actual[0] == expected[0]
|
11
|
+
if actual[1] > expected[1]
|
12
|
+
good = true
|
13
|
+
elsif actual[1] == expected[1] && actual[2] >= expected[2]
|
14
|
+
good = true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
good
|
19
|
+
end
|
20
|
+
|
21
|
+
MAJOR = 1
|
22
|
+
MINOR = 1
|
23
|
+
TINY = 0
|
24
|
+
|
25
|
+
STRING = [MAJOR, MINOR, TINY].join(".")
|
26
|
+
|
27
|
+
SSH_REQUIRED = [1,0,8]
|
28
|
+
SFTP_REQUIRED = [1,1,0]
|
29
|
+
end
|
30
|
+
end
|