capistrano 1.1.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/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
|