mbailey-capistrano 2.5.5
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.rdoc +761 -0
- data/Manifest +104 -0
- data/README.rdoc +66 -0
- data/Rakefile +34 -0
- data/bin/cap +4 -0
- data/bin/capify +78 -0
- data/examples/sample.rb +14 -0
- data/lib/capistrano/callback.rb +45 -0
- data/lib/capistrano/cli/execute.rb +84 -0
- data/lib/capistrano/cli/help.rb +125 -0
- data/lib/capistrano/cli/help.txt +75 -0
- data/lib/capistrano/cli/options.rb +224 -0
- data/lib/capistrano/cli/ui.rb +40 -0
- data/lib/capistrano/cli.rb +47 -0
- data/lib/capistrano/command.rb +283 -0
- data/lib/capistrano/configuration/actions/file_transfer.rb +47 -0
- data/lib/capistrano/configuration/actions/inspect.rb +46 -0
- data/lib/capistrano/configuration/actions/invocation.rb +293 -0
- data/lib/capistrano/configuration/callbacks.rb +148 -0
- data/lib/capistrano/configuration/connections.rb +200 -0
- data/lib/capistrano/configuration/execution.rb +132 -0
- data/lib/capistrano/configuration/loading.rb +197 -0
- data/lib/capistrano/configuration/namespaces.rb +197 -0
- data/lib/capistrano/configuration/roles.rb +73 -0
- data/lib/capistrano/configuration/servers.rb +85 -0
- data/lib/capistrano/configuration/variables.rb +127 -0
- data/lib/capistrano/configuration.rb +43 -0
- data/lib/capistrano/errors.rb +15 -0
- data/lib/capistrano/extensions.rb +57 -0
- data/lib/capistrano/logger.rb +59 -0
- data/lib/capistrano/processable.rb +53 -0
- data/lib/capistrano/recipes/compat.rb +32 -0
- data/lib/capistrano/recipes/deploy/dependencies.rb +44 -0
- data/lib/capistrano/recipes/deploy/local_dependency.rb +54 -0
- data/lib/capistrano/recipes/deploy/remote_dependency.rb +105 -0
- data/lib/capistrano/recipes/deploy/scm/accurev.rb +169 -0
- data/lib/capistrano/recipes/deploy/scm/base.rb +196 -0
- data/lib/capistrano/recipes/deploy/scm/bzr.rb +83 -0
- data/lib/capistrano/recipes/deploy/scm/cvs.rb +152 -0
- data/lib/capistrano/recipes/deploy/scm/darcs.rb +85 -0
- data/lib/capistrano/recipes/deploy/scm/git.rb +271 -0
- data/lib/capistrano/recipes/deploy/scm/mercurial.rb +137 -0
- data/lib/capistrano/recipes/deploy/scm/none.rb +44 -0
- data/lib/capistrano/recipes/deploy/scm/perforce.rb +133 -0
- data/lib/capistrano/recipes/deploy/scm/subversion.rb +121 -0
- data/lib/capistrano/recipes/deploy/scm.rb +19 -0
- data/lib/capistrano/recipes/deploy/strategy/base.rb +79 -0
- data/lib/capistrano/recipes/deploy/strategy/checkout.rb +20 -0
- data/lib/capistrano/recipes/deploy/strategy/copy.rb +210 -0
- data/lib/capistrano/recipes/deploy/strategy/export.rb +20 -0
- data/lib/capistrano/recipes/deploy/strategy/remote.rb +52 -0
- data/lib/capistrano/recipes/deploy/strategy/remote_cache.rb +56 -0
- data/lib/capistrano/recipes/deploy/strategy.rb +19 -0
- data/lib/capistrano/recipes/deploy/templates/maintenance.rhtml +53 -0
- data/lib/capistrano/recipes/deploy.rb +562 -0
- data/lib/capistrano/recipes/standard.rb +37 -0
- data/lib/capistrano/recipes/templates/maintenance.rhtml +53 -0
- data/lib/capistrano/recipes/upgrade.rb +33 -0
- data/lib/capistrano/role.rb +102 -0
- data/lib/capistrano/server_definition.rb +56 -0
- data/lib/capistrano/shell.rb +260 -0
- data/lib/capistrano/ssh.rb +99 -0
- data/lib/capistrano/task_definition.rb +70 -0
- data/lib/capistrano/transfer.rb +216 -0
- data/lib/capistrano/version.rb +18 -0
- data/lib/capistrano.rb +2 -0
- data/setup.rb +1346 -0
- data/test/cli/execute_test.rb +132 -0
- data/test/cli/help_test.rb +165 -0
- data/test/cli/options_test.rb +317 -0
- data/test/cli/ui_test.rb +28 -0
- data/test/cli_test.rb +17 -0
- data/test/command_test.rb +286 -0
- data/test/configuration/actions/file_transfer_test.rb +61 -0
- data/test/configuration/actions/inspect_test.rb +65 -0
- data/test/configuration/actions/invocation_test.rb +224 -0
- data/test/configuration/callbacks_test.rb +220 -0
- data/test/configuration/connections_test.rb +349 -0
- data/test/configuration/execution_test.rb +175 -0
- data/test/configuration/loading_test.rb +132 -0
- data/test/configuration/namespace_dsl_test.rb +311 -0
- data/test/configuration/roles_test.rb +144 -0
- data/test/configuration/servers_test.rb +121 -0
- data/test/configuration/variables_test.rb +184 -0
- data/test/configuration_test.rb +88 -0
- data/test/deploy/local_dependency_test.rb +76 -0
- data/test/deploy/remote_dependency_test.rb +114 -0
- data/test/deploy/scm/accurev_test.rb +23 -0
- data/test/deploy/scm/base_test.rb +55 -0
- data/test/deploy/scm/git_test.rb +167 -0
- data/test/deploy/scm/mercurial_test.rb +129 -0
- data/test/deploy/strategy/copy_test.rb +258 -0
- data/test/extensions_test.rb +69 -0
- data/test/fixtures/cli_integration.rb +5 -0
- data/test/fixtures/config.rb +5 -0
- data/test/fixtures/custom.rb +3 -0
- data/test/logger_test.rb +123 -0
- data/test/role_test.rb +11 -0
- data/test/server_definition_test.rb +121 -0
- data/test/shell_test.rb +90 -0
- data/test/ssh_test.rb +104 -0
- data/test/task_definition_test.rb +101 -0
- data/test/transfer_test.rb +160 -0
- data/test/utils.rb +38 -0
- metadata +205 -0
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'capistrano/recipes/deploy/scm/base'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Capistrano
|
5
|
+
module Deploy
|
6
|
+
module SCM
|
7
|
+
|
8
|
+
# Implements the Capistrano SCM interface for the Subversion revision
|
9
|
+
# control system (http://subversion.tigris.org).
|
10
|
+
class Subversion < Base
|
11
|
+
# Sets the default command name for this SCM. Users may override this
|
12
|
+
# by setting the :scm_command variable.
|
13
|
+
default_command "svn"
|
14
|
+
|
15
|
+
# Subversion understands 'HEAD' to refer to the latest revision in the
|
16
|
+
# repository.
|
17
|
+
def head
|
18
|
+
"HEAD"
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns the command that will check out the given revision to the
|
22
|
+
# given destination.
|
23
|
+
def checkout(revision, destination)
|
24
|
+
scm :checkout, arguments, verbose, authentication, "-r#{revision}", repository, destination
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns the command that will do an "svn update" to the given
|
28
|
+
# revision, for the working copy at the given destination.
|
29
|
+
def sync(revision, destination)
|
30
|
+
scm :update, arguments, verbose, authentication, "-r#{revision}", destination
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns the command that will do an "svn export" of the given revision
|
34
|
+
# to the given destination.
|
35
|
+
def export(revision, destination)
|
36
|
+
scm :export, arguments, verbose, authentication, "-r#{revision}", repository, destination
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the command that will do an "svn diff" for the two revisions.
|
40
|
+
def diff(from, to=nil)
|
41
|
+
scm :diff, repository, authentication, "-r#{from}:#{to || head}"
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns an "svn log" command for the two revisions.
|
45
|
+
def log(from, to=nil)
|
46
|
+
scm :log, repository, authentication, "-r#{from}:#{to || head}"
|
47
|
+
end
|
48
|
+
|
49
|
+
# Attempts to translate the given revision identifier to a "real"
|
50
|
+
# revision. If the identifier is an integer, it will simply be returned.
|
51
|
+
# Otherwise, this will yield a string of the commands it needs to be
|
52
|
+
# executed (svn info), and will extract the revision from the response.
|
53
|
+
def query_revision(revision)
|
54
|
+
return revision if revision =~ /^\d+$/
|
55
|
+
command = scm(:info, repository, authentication, "-r#{revision}")
|
56
|
+
result = yield(command)
|
57
|
+
yaml = YAML.load(result)
|
58
|
+
raise "tried to run `#{command}' and got unexpected result #{result.inspect}" unless Hash === yaml
|
59
|
+
yaml['Last Changed Rev'] || yaml['Revision']
|
60
|
+
end
|
61
|
+
|
62
|
+
# Increments the given revision number and returns it.
|
63
|
+
def next_revision(revision)
|
64
|
+
revision.to_i + 1
|
65
|
+
end
|
66
|
+
|
67
|
+
# Determines what the response should be for a particular bit of text
|
68
|
+
# from the SCM. Password prompts, connection requests, passphrases,
|
69
|
+
# etc. are handled here.
|
70
|
+
def handle_data(state, stream, text)
|
71
|
+
host = state[:channel][:host]
|
72
|
+
logger.info "[#{host} :: #{stream}] #{text}"
|
73
|
+
case text
|
74
|
+
when /\bpassword.*:/i
|
75
|
+
# subversion is prompting for a password
|
76
|
+
"#{scm_password_prompt}\n"
|
77
|
+
when %r{\(yes/no\)}
|
78
|
+
# subversion is asking whether or not to connect
|
79
|
+
"yes\n"
|
80
|
+
when /passphrase/i
|
81
|
+
# subversion is asking for the passphrase for the user's key
|
82
|
+
"#{variable(:scm_passphrase)}\n"
|
83
|
+
when /The entry \'(.+?)\' is no longer a directory/
|
84
|
+
raise Capistrano::Error, "subversion can't update because directory '#{$1}' was replaced. Please add it to svn:ignore."
|
85
|
+
when /accept \(t\)emporarily/
|
86
|
+
# subversion is asking whether to accept the certificate
|
87
|
+
"t\n"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
# If a username is configured for the SCM, return the command-line
|
94
|
+
# switches for that. Note that we don't need to return the password
|
95
|
+
# switch, since Capistrano will check for that prompt in the output
|
96
|
+
# and will respond appropriately.
|
97
|
+
def authentication
|
98
|
+
username = variable(:scm_username)
|
99
|
+
return "" unless username
|
100
|
+
result = "--username #{variable(:scm_username)} "
|
101
|
+
result << "--password #{variable(:scm_password)} " unless variable(:scm_auth_cache) || variable(:scm_prefer_prompt)
|
102
|
+
result << "--no-auth-cache " unless variable(:scm_auth_cache)
|
103
|
+
result
|
104
|
+
end
|
105
|
+
|
106
|
+
# If verbose output is requested, return nil, otherwise return the
|
107
|
+
# command-line switch for "quiet" ("-q").
|
108
|
+
def verbose
|
109
|
+
variable(:scm_verbose) ? nil : "-q"
|
110
|
+
end
|
111
|
+
|
112
|
+
def scm_password_prompt
|
113
|
+
@scm_password_prompt ||= variable(:scm_password) ||
|
114
|
+
variable(:password) ||
|
115
|
+
Capistrano::CLI.password_prompt("Subversion password: ")
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Capistrano
|
2
|
+
module Deploy
|
3
|
+
module SCM
|
4
|
+
def self.new(scm, config={})
|
5
|
+
scm_file = "capistrano/recipes/deploy/scm/#{scm}"
|
6
|
+
require(scm_file)
|
7
|
+
|
8
|
+
scm_const = scm.to_s.capitalize.gsub(/_(.)/) { $1.upcase }
|
9
|
+
if const_defined?(scm_const)
|
10
|
+
const_get(scm_const).new(config)
|
11
|
+
else
|
12
|
+
raise Capistrano::Error, "could not find `#{name}::#{scm_const}' in `#{scm_file}'"
|
13
|
+
end
|
14
|
+
rescue LoadError
|
15
|
+
raise Capistrano::Error, "could not find any SCM named `#{scm}'"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'capistrano/recipes/deploy/dependencies'
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
module Deploy
|
5
|
+
module Strategy
|
6
|
+
|
7
|
+
# This class defines the abstract interface for all Capistrano
|
8
|
+
# deployment strategies. Subclasses must implement at least the
|
9
|
+
# #deploy! method.
|
10
|
+
class Base
|
11
|
+
attr_reader :configuration
|
12
|
+
|
13
|
+
# Instantiates a strategy with a reference to the given configuration.
|
14
|
+
def initialize(config={})
|
15
|
+
@configuration = config
|
16
|
+
end
|
17
|
+
|
18
|
+
# Executes the necessary commands to deploy the revision of the source
|
19
|
+
# code identified by the +revision+ variable. Additionally, this
|
20
|
+
# should write the value of the +revision+ variable to a file called
|
21
|
+
# REVISION, in the base of the deployed revision. This file is used by
|
22
|
+
# other tasks, to perform diffs and such.
|
23
|
+
def deploy!
|
24
|
+
raise NotImplementedError, "`deploy!' is not implemented by #{self.class.name}"
|
25
|
+
end
|
26
|
+
|
27
|
+
# Performs a check on the remote hosts to determine whether everything
|
28
|
+
# is setup such that a deploy could succeed.
|
29
|
+
def check!
|
30
|
+
Dependencies.new(configuration) do |d|
|
31
|
+
d.remote.directory(configuration[:releases_path]).or("`#{configuration[:releases_path]}' does not exist. Please run `cap deploy:setup'.")
|
32
|
+
d.remote.writable(configuration[:deploy_to]).or("You do not have permissions to write to `#{configuration[:deploy_to]}'.")
|
33
|
+
d.remote.writable(configuration[:releases_path]).or("You do not have permissions to write to `#{configuration[:releases_path]}'.")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
# This is to allow helper methods like "run" and "put" to be more
|
40
|
+
# easily accessible to strategy implementations.
|
41
|
+
def method_missing(sym, *args, &block)
|
42
|
+
if configuration.respond_to?(sym)
|
43
|
+
configuration.send(sym, *args, &block)
|
44
|
+
else
|
45
|
+
super
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# A wrapper for Kernel#system that logs the command being executed.
|
50
|
+
def system(*args)
|
51
|
+
cmd = args.join(' ')
|
52
|
+
if RUBY_PLATFORM =~ /win32/
|
53
|
+
cmd.gsub!('/','\\') # Replace / with \\
|
54
|
+
cmd.gsub!(/^cd /,'cd /D ') # Replace cd with cd /D
|
55
|
+
cmd.gsub!(/&& cd /,'&& cd /D ') # Replace cd with cd /D
|
56
|
+
logger.trace "executing locally: #{cmd}"
|
57
|
+
super(cmd)
|
58
|
+
else
|
59
|
+
logger.trace "executing locally: #{cmd}"
|
60
|
+
super
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def logger
|
67
|
+
@logger ||= configuration[:logger] || Capistrano::Logger.new(:output => STDOUT)
|
68
|
+
end
|
69
|
+
|
70
|
+
# The revision to deploy. Must return a real revision identifier,
|
71
|
+
# and not a pseudo-id.
|
72
|
+
def revision
|
73
|
+
configuration[:real_revision]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'capistrano/recipes/deploy/strategy/remote'
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
module Deploy
|
5
|
+
module Strategy
|
6
|
+
|
7
|
+
# Implements the deployment strategy which does an SCM checkout on each
|
8
|
+
# target host. This is the default deployment strategy for Capistrano.
|
9
|
+
class Checkout < Remote
|
10
|
+
protected
|
11
|
+
|
12
|
+
# Returns the SCM's checkout command for the revision to deploy.
|
13
|
+
def command
|
14
|
+
@command ||= source.checkout(revision, configuration[:release_path])
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
require 'capistrano/recipes/deploy/strategy/base'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'tempfile' # Dir.tmpdir
|
4
|
+
|
5
|
+
module Capistrano
|
6
|
+
module Deploy
|
7
|
+
module Strategy
|
8
|
+
|
9
|
+
# This class implements the strategy for deployments which work
|
10
|
+
# by preparing the source code locally, compressing it, copying the
|
11
|
+
# file to each target host, and uncompressing it to the deployment
|
12
|
+
# directory.
|
13
|
+
#
|
14
|
+
# By default, the SCM checkout command is used to obtain the local copy
|
15
|
+
# of the source code. If you would rather use the export operation,
|
16
|
+
# you can set the :copy_strategy variable to :export.
|
17
|
+
#
|
18
|
+
# set :copy_strategy, :export
|
19
|
+
#
|
20
|
+
# For even faster deployments, you can set the :copy_cache variable to
|
21
|
+
# true. This will cause deployments to do a new checkout of your
|
22
|
+
# repository to a new directory, and then copy that checkout. Subsequent
|
23
|
+
# deploys will just resync that copy, rather than doing an entirely new
|
24
|
+
# checkout. Additionally, you can specify file patterns to exclude from
|
25
|
+
# the copy when using :copy_cache; just set the :copy_exclude variable
|
26
|
+
# to a file glob (or an array of globs).
|
27
|
+
#
|
28
|
+
# set :copy_cache, true
|
29
|
+
# set :copy_exclude, ".git/*"
|
30
|
+
#
|
31
|
+
# Note that :copy_strategy is ignored when :copy_cache is set. Also, if
|
32
|
+
# you want the copy cache put somewhere specific, you can set the variable
|
33
|
+
# to the path you want, instead of merely 'true':
|
34
|
+
#
|
35
|
+
# set :copy_cache, "/tmp/caches/myapp"
|
36
|
+
#
|
37
|
+
# This deployment strategy also supports a special variable,
|
38
|
+
# :copy_compression, which must be one of :gzip, :bz2, or
|
39
|
+
# :zip, and which specifies how the source should be compressed for
|
40
|
+
# transmission to each host.
|
41
|
+
class Copy < Base
|
42
|
+
# Obtains a copy of the source code locally (via the #command method),
|
43
|
+
# compresses it to a single file, copies that file to all target
|
44
|
+
# servers, and uncompresses it on each of them into the deployment
|
45
|
+
# directory.
|
46
|
+
def deploy!
|
47
|
+
if copy_cache
|
48
|
+
if File.exists?(copy_cache)
|
49
|
+
logger.debug "refreshing local cache to revision #{revision} at #{copy_cache}"
|
50
|
+
system(source.sync(revision, copy_cache))
|
51
|
+
else
|
52
|
+
logger.debug "preparing local cache at #{copy_cache}"
|
53
|
+
system(source.checkout(revision, copy_cache))
|
54
|
+
end
|
55
|
+
|
56
|
+
logger.debug "copying cache to deployment staging area #{destination}"
|
57
|
+
Dir.chdir(copy_cache) do
|
58
|
+
FileUtils.mkdir_p(destination)
|
59
|
+
queue = Dir.glob("*", File::FNM_DOTMATCH)
|
60
|
+
while queue.any?
|
61
|
+
item = queue.shift
|
62
|
+
name = File.basename(item)
|
63
|
+
|
64
|
+
next if name == "." || name == ".."
|
65
|
+
next if copy_exclude.any? { |pattern| File.fnmatch(pattern, item) }
|
66
|
+
|
67
|
+
if File.symlink?(item)
|
68
|
+
FileUtils.ln_s(File.readlink(File.join(copy_cache, item)), File.join(destination, item))
|
69
|
+
elsif File.directory?(item)
|
70
|
+
queue += Dir.glob("#{item}/*", File::FNM_DOTMATCH)
|
71
|
+
FileUtils.mkdir(File.join(destination, item))
|
72
|
+
else
|
73
|
+
FileUtils.ln(File.join(copy_cache, item), File.join(destination, item))
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
else
|
78
|
+
logger.debug "getting (via #{copy_strategy}) revision #{revision} to #{destination}"
|
79
|
+
system(command)
|
80
|
+
|
81
|
+
if copy_exclude.any?
|
82
|
+
logger.debug "processing exclusions..."
|
83
|
+
if copy_exclude.any?
|
84
|
+
copy_exclude.each do |pattern|
|
85
|
+
delete_list = Dir.glob(File.join(destination, pattern), File::FNM_DOTMATCH)
|
86
|
+
# avoid the /.. trap that deletes the parent directories
|
87
|
+
delete_list.delete_if { |dir| dir =~ /\/\.\.$/ }
|
88
|
+
FileUtils.rm_rf(delete_list.compact)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
File.open(File.join(destination, "REVISION"), "w") { |f| f.puts(revision) }
|
95
|
+
|
96
|
+
logger.trace "compressing #{destination} to #{filename}"
|
97
|
+
Dir.chdir(tmpdir) { system(compress(File.basename(destination), File.basename(filename)).join(" ")) }
|
98
|
+
|
99
|
+
upload(filename, remote_filename)
|
100
|
+
run "cd #{configuration[:releases_path]} && #{decompress(remote_filename).join(" ")} && rm #{remote_filename}"
|
101
|
+
ensure
|
102
|
+
FileUtils.rm filename rescue nil
|
103
|
+
FileUtils.rm_rf destination rescue nil
|
104
|
+
end
|
105
|
+
|
106
|
+
def check!
|
107
|
+
super.check do |d|
|
108
|
+
d.local.command(source.local.command) if source.local.command
|
109
|
+
d.local.command(compress(nil, nil).first)
|
110
|
+
d.remote.command(decompress(nil).first)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns the location of the local copy cache, if the strategy should
|
115
|
+
# use a local cache + copy instead of a new checkout/export every
|
116
|
+
# time. Returns +nil+ unless :copy_cache has been set. If :copy_cache
|
117
|
+
# is +true+, a default cache location will be returned.
|
118
|
+
def copy_cache
|
119
|
+
@copy_cache ||= configuration[:copy_cache] == true ?
|
120
|
+
File.join(Dir.tmpdir, configuration[:application]) :
|
121
|
+
configuration[:copy_cache]
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
# Specify patterns to exclude from the copy. This is only valid
|
127
|
+
# when using a local cache.
|
128
|
+
def copy_exclude
|
129
|
+
@copy_exclude ||= Array(configuration.fetch(:copy_exclude, []))
|
130
|
+
end
|
131
|
+
|
132
|
+
# Returns the basename of the release_path, which will be used to
|
133
|
+
# name the local copy and archive file.
|
134
|
+
def destination
|
135
|
+
@destination ||= File.join(tmpdir, File.basename(configuration[:release_path]))
|
136
|
+
end
|
137
|
+
|
138
|
+
# Returns the value of the :copy_strategy variable, defaulting to
|
139
|
+
# :checkout if it has not been set.
|
140
|
+
def copy_strategy
|
141
|
+
@copy_strategy ||= configuration.fetch(:copy_strategy, :checkout)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Should return the command(s) necessary to obtain the source code
|
145
|
+
# locally.
|
146
|
+
def command
|
147
|
+
@command ||= case copy_strategy
|
148
|
+
when :checkout
|
149
|
+
source.checkout(revision, destination)
|
150
|
+
when :export
|
151
|
+
source.export(revision, destination)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Returns the name of the file that the source code will be
|
156
|
+
# compressed to.
|
157
|
+
def filename
|
158
|
+
@filename ||= File.join(tmpdir, "#{File.basename(destination)}.#{compression.extension}")
|
159
|
+
end
|
160
|
+
|
161
|
+
# The directory to which the copy should be checked out
|
162
|
+
def tmpdir
|
163
|
+
@tmpdir ||= configuration[:copy_dir] || Dir.tmpdir
|
164
|
+
end
|
165
|
+
|
166
|
+
# The directory on the remote server to which the archive should be
|
167
|
+
# copied
|
168
|
+
def remote_dir
|
169
|
+
@remote_dir ||= configuration[:copy_remote_dir] || "/tmp"
|
170
|
+
end
|
171
|
+
|
172
|
+
# The location on the remote server where the file should be
|
173
|
+
# temporarily stored.
|
174
|
+
def remote_filename
|
175
|
+
@remote_filename ||= File.join(remote_dir, File.basename(filename))
|
176
|
+
end
|
177
|
+
|
178
|
+
# A struct for representing the specifics of a compression type.
|
179
|
+
# Commands are arrays, where the first element is the utility to be
|
180
|
+
# used to perform the compression or decompression.
|
181
|
+
Compression = Struct.new(:extension, :compress_command, :decompress_command)
|
182
|
+
|
183
|
+
# The compression method to use, defaults to :gzip.
|
184
|
+
def compression
|
185
|
+
type = configuration[:copy_compression] || :gzip
|
186
|
+
case type
|
187
|
+
when :gzip, :gz then Compression.new("tar.gz", %w(tar czf), %w(tar xzf))
|
188
|
+
when :bzip2, :bz2 then Compression.new("tar.bz2", %w(tar cjf), %w(tar xjf))
|
189
|
+
when :zip then Compression.new("zip", %w(zip -qr), %w(unzip -q))
|
190
|
+
else raise ArgumentError, "invalid compression type #{type.inspect}"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# Returns the command necessary to compress the given directory
|
195
|
+
# into the given file.
|
196
|
+
def compress(directory, file)
|
197
|
+
compression.compress_command + [file, directory]
|
198
|
+
end
|
199
|
+
|
200
|
+
# Returns the command necessary to decompress the given file,
|
201
|
+
# relative to the current working directory. It must also
|
202
|
+
# preserve the directory structure in the file.
|
203
|
+
def decompress(file)
|
204
|
+
compression.decompress_command + [file]
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'capistrano/recipes/deploy/strategy/remote'
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
module Deploy
|
5
|
+
module Strategy
|
6
|
+
|
7
|
+
# Implements the deployment strategy which does an SCM export on each
|
8
|
+
# target host.
|
9
|
+
class Export < Remote
|
10
|
+
protected
|
11
|
+
|
12
|
+
# Returns the SCM's export command for the revision to deploy.
|
13
|
+
def command
|
14
|
+
@command ||= source.export(revision, configuration[:release_path])
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'capistrano/recipes/deploy/strategy/base'
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
module Deploy
|
5
|
+
module Strategy
|
6
|
+
|
7
|
+
# An abstract superclass, which forms the base for all deployment
|
8
|
+
# strategies which work by grabbing the code from the repository directly
|
9
|
+
# from remote host. This includes deploying by checkout (the default),
|
10
|
+
# and deploying by export.
|
11
|
+
class Remote < Base
|
12
|
+
# Executes the SCM command for this strategy and writes the REVISION
|
13
|
+
# mark file to each host.
|
14
|
+
def deploy!
|
15
|
+
scm_run "#{command} && #{mark}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def check!
|
19
|
+
super.check do |d|
|
20
|
+
d.remote.command(source.command)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
# Runs the given command, filtering output back through the
|
27
|
+
# #handle_data filter of the SCM implementation.
|
28
|
+
def scm_run(command)
|
29
|
+
run(command) do |ch,stream,text|
|
30
|
+
ch[:state] ||= { :channel => ch }
|
31
|
+
output = source.handle_data(ch[:state], stream, text)
|
32
|
+
ch.send_data(output) if output
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# An abstract method which must be overridden in subclasses, to
|
37
|
+
# return the actual SCM command(s) which must be executed on each
|
38
|
+
# target host in order to perform the deployment.
|
39
|
+
def command
|
40
|
+
raise NotImplementedError, "`command' is not implemented by #{self.class.name}"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the command which will write the identifier of the
|
44
|
+
# revision being deployed to the REVISION file on each host.
|
45
|
+
def mark
|
46
|
+
"(echo #{revision} > #{configuration[:release_path]}/REVISION)"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'capistrano/recipes/deploy/strategy/remote'
|
2
|
+
|
3
|
+
module Capistrano
|
4
|
+
module Deploy
|
5
|
+
module Strategy
|
6
|
+
|
7
|
+
# Implements the deployment strategy that keeps a cached checkout of
|
8
|
+
# the source code on each remote server. Each deploy simply updates the
|
9
|
+
# cached checkout, and then does a copy from the cached copy to the
|
10
|
+
# final deployment location.
|
11
|
+
class RemoteCache < Remote
|
12
|
+
# Executes the SCM command for this strategy and writes the REVISION
|
13
|
+
# mark file to each host.
|
14
|
+
def deploy!
|
15
|
+
update_repository_cache
|
16
|
+
copy_repository_cache
|
17
|
+
end
|
18
|
+
|
19
|
+
def check!
|
20
|
+
super.check do |d|
|
21
|
+
d.remote.writable(shared_path)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def repository_cache
|
28
|
+
File.join(shared_path, configuration[:repository_cache] || "cached-copy")
|
29
|
+
end
|
30
|
+
|
31
|
+
def update_repository_cache
|
32
|
+
logger.trace "updating the cached checkout on all servers"
|
33
|
+
command = "if [ -d #{repository_cache} ]; then " +
|
34
|
+
"#{source.sync(revision, repository_cache)}; " +
|
35
|
+
"else #{source.checkout(revision, repository_cache)}; fi"
|
36
|
+
scm_run(command)
|
37
|
+
end
|
38
|
+
|
39
|
+
def copy_repository_cache
|
40
|
+
logger.trace "copying the cached version to #{configuration[:release_path]}"
|
41
|
+
if copy_exclude.empty?
|
42
|
+
run "cp -RPp #{repository_cache} #{configuration[:release_path]} && #{mark}"
|
43
|
+
else
|
44
|
+
exclusions = copy_exclude.map { |e| "--exclude=\"#{e}\"" }.join(' ')
|
45
|
+
run "rsync -lrpt #{exclusions} #{repository_cache}/* #{configuration[:release_path]} && #{mark}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def copy_exclude
|
50
|
+
@copy_exclude ||= Array(configuration.fetch(:copy_exclude, []))
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Capistrano
|
2
|
+
module Deploy
|
3
|
+
module Strategy
|
4
|
+
def self.new(strategy, config={})
|
5
|
+
strategy_file = "capistrano/recipes/deploy/strategy/#{strategy}"
|
6
|
+
require(strategy_file)
|
7
|
+
|
8
|
+
strategy_const = strategy.to_s.capitalize.gsub(/_(.)/) { $1.upcase }
|
9
|
+
if const_defined?(strategy_const)
|
10
|
+
const_get(strategy_const).new(config)
|
11
|
+
else
|
12
|
+
raise Capistrano::Error, "could not find `#{name}::#{strategy_const}' in `#{strategy_file}'"
|
13
|
+
end
|
14
|
+
rescue LoadError
|
15
|
+
raise Capistrano::Error, "could not find any strategy named `#{strategy}'"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
|
2
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
3
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
4
|
+
|
5
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
6
|
+
|
7
|
+
<head>
|
8
|
+
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
|
9
|
+
<title>System down for maintenance</title>
|
10
|
+
|
11
|
+
<style type="text/css">
|
12
|
+
div.outer {
|
13
|
+
position: absolute;
|
14
|
+
left: 50%;
|
15
|
+
top: 50%;
|
16
|
+
width: 500px;
|
17
|
+
height: 300px;
|
18
|
+
margin-left: -260px;
|
19
|
+
margin-top: -150px;
|
20
|
+
}
|
21
|
+
|
22
|
+
.DialogBody {
|
23
|
+
margin: 0;
|
24
|
+
padding: 10px;
|
25
|
+
text-align: left;
|
26
|
+
border: 1px solid #ccc;
|
27
|
+
border-right: 1px solid #999;
|
28
|
+
border-bottom: 1px solid #999;
|
29
|
+
background-color: #fff;
|
30
|
+
}
|
31
|
+
|
32
|
+
body { background-color: #fff; }
|
33
|
+
</style>
|
34
|
+
</head>
|
35
|
+
|
36
|
+
<body>
|
37
|
+
|
38
|
+
<div class="outer">
|
39
|
+
<div class="DialogBody" style="text-align: center;">
|
40
|
+
<div style="text-align: center; width: 200px; margin: 0 auto;">
|
41
|
+
<p style="color: red; font-size: 16px; line-height: 20px;">
|
42
|
+
The system is down for <%= reason ? reason : "maintenance" %>
|
43
|
+
as of <%= Time.now.strftime("%H:%M %Z") %>.
|
44
|
+
</p>
|
45
|
+
<p style="color: #666;">
|
46
|
+
It'll be back <%= deadline ? deadline : "shortly" %>.
|
47
|
+
</p>
|
48
|
+
</div>
|
49
|
+
</div>
|
50
|
+
</div>
|
51
|
+
|
52
|
+
</body>
|
53
|
+
</html>
|