capistrano 2.0.0 → 2.15.11
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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.travis.yml +9 -0
- data/CHANGELOG +763 -18
- data/Gemfile +15 -0
- data/README.md +94 -0
- data/Rakefile +11 -0
- data/bin/cap +0 -0
- data/bin/capify +40 -25
- data/capistrano.gemspec +40 -0
- data/lib/capistrano/callback.rb +5 -1
- data/lib/capistrano/cli/execute.rb +10 -7
- data/lib/capistrano/cli/help.rb +39 -16
- data/lib/capistrano/cli/help.txt +44 -16
- data/lib/capistrano/cli/options.rb +71 -11
- data/lib/capistrano/cli/ui.rb +13 -1
- data/lib/capistrano/cli.rb +5 -5
- data/lib/capistrano/command.rb +215 -58
- data/lib/capistrano/configuration/actions/file_transfer.rb +29 -14
- data/lib/capistrano/configuration/actions/inspect.rb +3 -3
- data/lib/capistrano/configuration/actions/invocation.rb +224 -22
- data/lib/capistrano/configuration/alias_task.rb +26 -0
- data/lib/capistrano/configuration/callbacks.rb +26 -27
- data/lib/capistrano/configuration/connections.rb +130 -52
- data/lib/capistrano/configuration/execution.rb +34 -18
- data/lib/capistrano/configuration/loading.rb +99 -6
- data/lib/capistrano/configuration/log_formatters.rb +75 -0
- data/lib/capistrano/configuration/namespaces.rb +45 -12
- data/lib/capistrano/configuration/roles.rb +34 -2
- data/lib/capistrano/configuration/servers.rb +51 -10
- data/lib/capistrano/configuration/variables.rb +3 -3
- data/lib/capistrano/configuration.rb +20 -4
- data/lib/capistrano/errors.rb +12 -8
- data/lib/capistrano/ext/multistage.rb +67 -0
- data/lib/capistrano/ext/string.rb +5 -0
- data/lib/capistrano/extensions.rb +1 -1
- data/lib/capistrano/fix_rake_deprecated_dsl.rb +8 -0
- data/lib/capistrano/logger.rb +112 -5
- data/lib/capistrano/processable.rb +55 -0
- data/lib/capistrano/recipes/compat.rb +2 -2
- data/lib/capistrano/recipes/deploy/assets.rb +202 -0
- data/lib/capistrano/recipes/deploy/dependencies.rb +2 -2
- data/lib/capistrano/recipes/deploy/local_dependency.rb +10 -2
- data/lib/capistrano/recipes/deploy/remote_dependency.rb +54 -2
- data/lib/capistrano/recipes/deploy/scm/accurev.rb +169 -0
- data/lib/capistrano/recipes/deploy/scm/base.rb +31 -11
- data/lib/capistrano/recipes/deploy/scm/bzr.rb +14 -14
- data/lib/capistrano/recipes/deploy/scm/cvs.rb +11 -9
- data/lib/capistrano/recipes/deploy/scm/darcs.rb +12 -1
- data/lib/capistrano/recipes/deploy/scm/git.rb +299 -0
- data/lib/capistrano/recipes/deploy/scm/mercurial.rb +23 -15
- data/lib/capistrano/recipes/deploy/scm/none.rb +55 -0
- data/lib/capistrano/recipes/deploy/scm/perforce.rb +54 -28
- data/lib/capistrano/recipes/deploy/scm/subversion.rb +36 -18
- data/lib/capistrano/recipes/deploy/scm.rb +1 -1
- data/lib/capistrano/recipes/deploy/strategy/base.rb +32 -4
- data/lib/capistrano/recipes/deploy/strategy/copy.rb +238 -43
- data/lib/capistrano/recipes/deploy/strategy/remote.rb +1 -1
- data/lib/capistrano/recipes/deploy/strategy/remote_cache.rb +11 -1
- data/lib/capistrano/recipes/deploy/strategy/unshared_remote_cache.rb +21 -0
- data/lib/capistrano/recipes/deploy/strategy.rb +4 -3
- data/lib/capistrano/recipes/deploy.rb +265 -123
- data/lib/capistrano/recipes/standard.rb +1 -1
- data/lib/capistrano/role.rb +102 -0
- data/lib/capistrano/server_definition.rb +6 -1
- data/lib/capistrano/shell.rb +30 -33
- data/lib/capistrano/ssh.rb +46 -60
- data/lib/capistrano/task_definition.rb +16 -8
- data/lib/capistrano/transfer.rb +218 -0
- data/lib/capistrano/version.rb +8 -17
- data/lib/capistrano.rb +4 -1
- data/test/cli/execute_test.rb +3 -3
- data/test/cli/help_test.rb +33 -7
- data/test/cli/options_test.rb +109 -6
- data/test/cli/ui_test.rb +2 -2
- data/test/cli_test.rb +3 -3
- data/test/command_test.rb +144 -124
- data/test/configuration/actions/file_transfer_test.rb +41 -20
- data/test/configuration/actions/inspect_test.rb +21 -7
- data/test/configuration/actions/invocation_test.rb +141 -30
- data/test/configuration/alias_task_test.rb +118 -0
- data/test/configuration/callbacks_test.rb +41 -46
- data/test/configuration/connections_test.rb +187 -36
- data/test/configuration/execution_test.rb +18 -2
- data/test/configuration/loading_test.rb +33 -4
- data/test/configuration/namespace_dsl_test.rb +54 -5
- data/test/configuration/roles_test.rb +114 -4
- data/test/configuration/servers_test.rb +97 -4
- data/test/configuration/variables_test.rb +12 -2
- data/test/configuration_test.rb +9 -13
- data/test/deploy/local_dependency_test.rb +76 -0
- data/test/deploy/remote_dependency_test.rb +146 -0
- data/test/deploy/scm/accurev_test.rb +23 -0
- data/test/deploy/scm/base_test.rb +1 -1
- data/test/deploy/scm/bzr_test.rb +51 -0
- data/test/deploy/scm/darcs_test.rb +37 -0
- data/test/deploy/scm/git_test.rb +274 -0
- data/test/deploy/scm/mercurial_test.rb +134 -0
- data/test/deploy/scm/none_test.rb +35 -0
- data/test/deploy/scm/perforce_test.rb +23 -0
- data/test/deploy/scm/subversion_test.rb +68 -0
- data/test/deploy/strategy/copy_test.rb +240 -26
- data/test/extensions_test.rb +2 -2
- data/test/logger_formatting_test.rb +149 -0
- data/test/logger_test.rb +13 -2
- data/test/recipes_test.rb +25 -0
- data/test/role_test.rb +11 -0
- data/test/server_definition_test.rb +15 -2
- data/test/shell_test.rb +33 -1
- data/test/ssh_test.rb +40 -24
- data/test/task_definition_test.rb +18 -2
- data/test/transfer_test.rb +168 -0
- data/test/utils.rb +28 -33
- data/test/version_test.rb +4 -17
- metadata +214 -100
- data/MIT-LICENSE +0 -20
- data/README +0 -43
- data/examples/sample.rb +0 -14
- data/lib/capistrano/gateway.rb +0 -131
- data/lib/capistrano/recipes/deploy/templates/maintenance.rhtml +0 -53
- data/lib/capistrano/recipes/upgrade.rb +0 -33
- data/lib/capistrano/upload.rb +0 -146
- data/test/gateway_test.rb +0 -167
- data/test/upload_test.rb +0 -131
|
@@ -7,7 +7,7 @@ load 'deploy'
|
|
|
7
7
|
map = { "diff_from_last_deploy" => "deploy:pending:diff",
|
|
8
8
|
"update" => "deploy:update",
|
|
9
9
|
"update_code" => "deploy:update_code",
|
|
10
|
-
"symlink" => "deploy:
|
|
10
|
+
"symlink" => "deploy:create_symlink",
|
|
11
11
|
"restart" => "deploy:restart",
|
|
12
12
|
"rollback" => "deploy:rollback",
|
|
13
13
|
"cleanup" => "deploy:cleanup",
|
|
@@ -29,4 +29,4 @@ task :spinner do
|
|
|
29
29
|
warn "[DEPRECATED] `spinner' is deprecated. Use `deploy:start' instead."
|
|
30
30
|
set :runner, fetch(:spinner_user, "app")
|
|
31
31
|
deploy.start
|
|
32
|
-
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
load 'deploy' unless defined?(_cset)
|
|
4
|
+
|
|
5
|
+
_cset :asset_env, "RAILS_GROUPS=assets"
|
|
6
|
+
_cset :assets_prefix, "assets"
|
|
7
|
+
_cset :shared_assets_prefix, "assets"
|
|
8
|
+
_cset :assets_role, [:web]
|
|
9
|
+
_cset :expire_assets_after, (3600 * 24 * 7)
|
|
10
|
+
_cset(:asset_manifest_prefix) { (`sprockets -v`.chomp < "3.0" ? "manifest" : ".sprockets-manifest") rescue "manifest" }
|
|
11
|
+
|
|
12
|
+
_cset :normalize_asset_timestamps, false
|
|
13
|
+
|
|
14
|
+
before 'deploy:finalize_update', 'deploy:assets:symlink'
|
|
15
|
+
after 'deploy:update_code', 'deploy:assets:precompile'
|
|
16
|
+
before 'deploy:assets:precompile', 'deploy:assets:update_asset_mtimes'
|
|
17
|
+
after 'deploy:cleanup', 'deploy:assets:clean_expired'
|
|
18
|
+
after 'deploy:rollback:revision', 'deploy:assets:rollback'
|
|
19
|
+
|
|
20
|
+
def shared_manifest_path
|
|
21
|
+
@shared_manifest_path ||= capture("ls #{shared_path.shellescape}/#{shared_assets_prefix}/#{asset_manifest_prefix}*").strip
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Parses manifest and returns array of uncompressed and compressed asset filenames with and without digests
|
|
25
|
+
# "Intelligently" determines format of string - supports YAML and JSON
|
|
26
|
+
def parse_manifest(str)
|
|
27
|
+
assets_hash = str[0,1] == '{' ? JSON.parse(str)['assets'] : YAML.load(str)
|
|
28
|
+
|
|
29
|
+
assets_hash.to_a.flatten.map {|a| [a, "#{a}.gz"] }.flatten
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
namespace :deploy do
|
|
33
|
+
namespace :assets do
|
|
34
|
+
desc <<-DESC
|
|
35
|
+
[internal] This task will set up a symlink to the shared directory \
|
|
36
|
+
for the assets directory. Assets are shared across deploys to avoid \
|
|
37
|
+
mid-deploy mismatches between old application html asking for assets \
|
|
38
|
+
and getting a 404 file not found error. The assets cache is shared \
|
|
39
|
+
for efficiency. If you customize the assets path prefix, override the \
|
|
40
|
+
:assets_prefix variable to match. If you customize shared assets path \
|
|
41
|
+
prefix, override :shared_assets_prefix variable to match.
|
|
42
|
+
DESC
|
|
43
|
+
task :symlink, :roles => lambda { assets_role }, :except => { :no_release => true } do
|
|
44
|
+
run <<-CMD.compact
|
|
45
|
+
rm -rf #{latest_release}/public/#{assets_prefix} &&
|
|
46
|
+
mkdir -p #{latest_release}/public &&
|
|
47
|
+
mkdir -p #{shared_path}/#{shared_assets_prefix} &&
|
|
48
|
+
ln -s #{shared_path}/#{shared_assets_prefix} #{latest_release}/public/#{assets_prefix}
|
|
49
|
+
CMD
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
desc <<-DESC
|
|
53
|
+
Run the asset precompilation rake task. You can specify the full path \
|
|
54
|
+
to the rake executable by setting the rake variable. You can also \
|
|
55
|
+
specify additional environment variables to pass to rake via the \
|
|
56
|
+
asset_env variable. The defaults are:
|
|
57
|
+
|
|
58
|
+
set :rake, "rake"
|
|
59
|
+
set :rails_env, "production"
|
|
60
|
+
set :asset_env, "RAILS_GROUPS=assets"
|
|
61
|
+
DESC
|
|
62
|
+
task :precompile, :roles => lambda { assets_role }, :except => { :no_release => true } do
|
|
63
|
+
run <<-CMD.compact
|
|
64
|
+
cd -- #{latest_release} &&
|
|
65
|
+
RAILS_ENV=#{rails_env.to_s.shellescape} #{asset_env} #{rake} assets:precompile
|
|
66
|
+
CMD
|
|
67
|
+
|
|
68
|
+
if capture("ls -1 #{shared_path.shellescape}/#{shared_assets_prefix}/#{asset_manifest_prefix}* | wc -l").to_i > 1
|
|
69
|
+
raise "More than one asset manifest file was found in '#{shared_path.shellescape}/#{shared_assets_prefix}'. If you are upgrading a Rails 3 application to Rails 4, follow these instructions: http://github.com/capistrano/capistrano/wiki/Upgrading-to-Rails-4#asset-pipeline"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Sync manifest filenames across servers if our manifest has a random filename
|
|
73
|
+
if shared_manifest_path =~ /#{asset_manifest_prefix}-.+\./
|
|
74
|
+
run <<-CMD.compact
|
|
75
|
+
[ -e #{shared_manifest_path.shellescape} ] || mv -- #{shared_path.shellescape}/#{shared_assets_prefix}/#{asset_manifest_prefix}* #{shared_manifest_path.shellescape}
|
|
76
|
+
CMD
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Copy manifest to release root (for clean_expired task)
|
|
80
|
+
run <<-CMD.compact
|
|
81
|
+
cp -- #{shared_manifest_path.shellescape} #{current_release.to_s.shellescape}/assets_manifest#{File.extname(shared_manifest_path)}
|
|
82
|
+
CMD
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
desc <<-DESC
|
|
86
|
+
[internal] Updates the mtimes for assets that are required by the current release.
|
|
87
|
+
This task runs before assets:precompile.
|
|
88
|
+
DESC
|
|
89
|
+
task :update_asset_mtimes, :roles => lambda { assets_role }, :except => { :no_release => true } do
|
|
90
|
+
# Fetch assets/manifest contents.
|
|
91
|
+
manifest_content = capture("[ -e '#{shared_path.shellescape}/#{shared_assets_prefix}/#{asset_manifest_prefix}*' ] && cat #{shared_path.shellescape}/#{shared_assets_prefix}/#{asset_manifest_prefix}* || echo").strip
|
|
92
|
+
|
|
93
|
+
if manifest_content != ""
|
|
94
|
+
current_assets = parse_manifest(manifest_content)
|
|
95
|
+
logger.info "Updating mtimes for ~#{current_assets.count} assets..."
|
|
96
|
+
put current_assets.map{|a| "#{shared_path}/#{shared_assets_prefix}/#{a}" }.join("\n"), "#{deploy_to}/TOUCH_ASSETS", :via => :scp
|
|
97
|
+
run <<-CMD.compact
|
|
98
|
+
cat #{deploy_to.shellescape}/TOUCH_ASSETS | while read asset; do
|
|
99
|
+
touch -c -- "$asset";
|
|
100
|
+
done &&
|
|
101
|
+
rm -f -- #{deploy_to.shellescape}/TOUCH_ASSETS
|
|
102
|
+
CMD
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
desc <<-DESC
|
|
107
|
+
Run the asset clean rake task. Use with caution, this will delete \
|
|
108
|
+
all of your compiled assets. You can specify the full path \
|
|
109
|
+
to the rake executable by setting the rake variable. You can also \
|
|
110
|
+
specify additional environment variables to pass to rake via the \
|
|
111
|
+
asset_env variable. The defaults are:
|
|
112
|
+
|
|
113
|
+
set :rake, "rake"
|
|
114
|
+
set :rails_env, "production"
|
|
115
|
+
set :asset_env, "RAILS_GROUPS=assets"
|
|
116
|
+
DESC
|
|
117
|
+
task :clean, :roles => lambda { assets_role }, :except => { :no_release => true } do
|
|
118
|
+
run "cd #{latest_release} && #{rake} RAILS_ENV=#{rails_env} #{asset_env} assets:clean"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
desc <<-DESC
|
|
122
|
+
Clean up any assets that haven't been deployed for more than :expire_assets_after seconds.
|
|
123
|
+
Default time to keep old assets is one week. Set the :expire_assets_after variable
|
|
124
|
+
to change the assets expiry time. Assets will only be deleted if they are not required by
|
|
125
|
+
an existing release.
|
|
126
|
+
DESC
|
|
127
|
+
task :clean_expired, :roles => lambda { assets_role }, :except => { :no_release => true } do
|
|
128
|
+
# Fetch all assets_manifest contents.
|
|
129
|
+
manifests_output = capture <<-CMD.compact
|
|
130
|
+
for manifest in #{releases_path.shellescape}/*/assets_manifest.*; do
|
|
131
|
+
cat -- "$manifest" 2> /dev/null && printf ':::' || true;
|
|
132
|
+
done
|
|
133
|
+
CMD
|
|
134
|
+
manifests = manifests_output.split(':::')
|
|
135
|
+
|
|
136
|
+
if manifests.empty?
|
|
137
|
+
logger.info "No manifests in #{releases_path}/*/assets_manifest.*"
|
|
138
|
+
else
|
|
139
|
+
logger.info "Fetched #{manifests.count} manifests from #{releases_path}/*/assets_manifest.*"
|
|
140
|
+
current_assets = Set.new
|
|
141
|
+
manifests.each do |content|
|
|
142
|
+
current_assets += parse_manifest(content)
|
|
143
|
+
end
|
|
144
|
+
current_assets += [File.basename(shared_manifest_path), "sources_manifest.yml"]
|
|
145
|
+
|
|
146
|
+
# Write the list of required assets to server.
|
|
147
|
+
logger.info "Writing required assets to #{deploy_to}/REQUIRED_ASSETS..."
|
|
148
|
+
escaped_assets = current_assets.sort.join("\n").gsub("\"", "\\\"") << "\n"
|
|
149
|
+
put escaped_assets, "#{deploy_to}/REQUIRED_ASSETS", :via => :scp
|
|
150
|
+
|
|
151
|
+
# Finds all files older than X minutes, then removes them if they are not referenced
|
|
152
|
+
# in REQUIRED_ASSETS.
|
|
153
|
+
expire_after_mins = (expire_assets_after.to_f / 60.0).to_i
|
|
154
|
+
logger.info "Removing assets that haven't been deployed for #{expire_after_mins} minutes..."
|
|
155
|
+
# LC_COLLATE=C tells the `sort` and `comm` commands to sort files in byte order.
|
|
156
|
+
run <<-CMD.compact
|
|
157
|
+
cd -- #{deploy_to.shellescape}/ &&
|
|
158
|
+
LC_COLLATE=C sort REQUIRED_ASSETS -o REQUIRED_ASSETS &&
|
|
159
|
+
cd -- #{shared_path.shellescape}/#{shared_assets_prefix}/ &&
|
|
160
|
+
for f in $(
|
|
161
|
+
find * -mmin +#{expire_after_mins.to_s.shellescape} -type f | LC_COLLATE=C sort |
|
|
162
|
+
LC_COLLATE=C comm -23 -- - #{deploy_to.shellescape}/REQUIRED_ASSETS
|
|
163
|
+
); do
|
|
164
|
+
echo "Removing unneeded asset: $f";
|
|
165
|
+
rm -f -- "$f";
|
|
166
|
+
done;
|
|
167
|
+
rm -f -- #{deploy_to.shellescape}/REQUIRED_ASSETS
|
|
168
|
+
CMD
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
desc <<-DESC
|
|
173
|
+
Rolls back assets to the previous release by symlinking the release's manifest
|
|
174
|
+
to shared/assets/manifest, and finally recompiling or regenerating nondigest assets.
|
|
175
|
+
DESC
|
|
176
|
+
task :rollback, :roles => lambda { assets_role }, :except => { :no_release => true } do
|
|
177
|
+
previous_manifest = capture("ls #{previous_release.shellescape}/assets_manifest.*").strip
|
|
178
|
+
if capture("[ -e #{previous_manifest.shellescape} ] && echo true || echo false").strip != 'true'
|
|
179
|
+
puts "#{previous_manifest} is missing! Cannot roll back assets. " <<
|
|
180
|
+
"Please run deploy:assets:precompile to update your assets when the rollback is finished."
|
|
181
|
+
else
|
|
182
|
+
# If the user is rolling back a Rails 4 app to Rails 3
|
|
183
|
+
if File.extname(previous_manifest) == '.yml' && File.extname(shared_manifest_path) == '.json'
|
|
184
|
+
# Remove the existing JSON manifest
|
|
185
|
+
run "rm -f -- #{shared_manifest_path.shellescape}"
|
|
186
|
+
|
|
187
|
+
# Restore the manifest to the Rails 3 path
|
|
188
|
+
restored_manifest_path = "#{shared_path.shellescape}/#{shared_assets_prefix}/manifest.yml"
|
|
189
|
+
else
|
|
190
|
+
# If the user is not rolling back from Rails 4 to 3, we just want to replace the current manifest
|
|
191
|
+
restored_manifest_path = shared_manifest_path
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
run <<-CMD.compact
|
|
195
|
+
cd -- #{previous_release.shellescape} &&
|
|
196
|
+
cp -f -- #{previous_manifest.shellescape} #{restored_manifest_path.shellescape} &&
|
|
197
|
+
[ -z "$(#{rake} -P | grep assets:precompile:nondigest)" ] || #{rake} RAILS_ENV=#{rails_env.to_s.shellescape} #{asset_env} assets:precompile:nondigest
|
|
198
|
+
CMD
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -30,7 +30,7 @@ module Capistrano
|
|
|
30
30
|
# file is found that matches the parameter, this returns true.
|
|
31
31
|
def find_in_path(utility)
|
|
32
32
|
path = (ENV['PATH'] || "").split(File::PATH_SEPARATOR)
|
|
33
|
-
suffixes =
|
|
33
|
+
suffixes = self.class.on_windows? ? self.class.windows_executable_extensions : [""]
|
|
34
34
|
|
|
35
35
|
path.each do |dir|
|
|
36
36
|
suffixes.each do |sfx|
|
|
@@ -41,6 +41,14 @@ module Capistrano
|
|
|
41
41
|
|
|
42
42
|
false
|
|
43
43
|
end
|
|
44
|
+
|
|
45
|
+
def self.on_windows?
|
|
46
|
+
RUBY_PLATFORM =~ /mswin|mingw/
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.windows_executable_extensions
|
|
50
|
+
%w(.exe .bat .com .cmd)
|
|
51
|
+
end
|
|
44
52
|
end
|
|
45
53
|
end
|
|
46
|
-
end
|
|
54
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'capistrano/errors'
|
|
2
|
+
|
|
1
3
|
module Capistrano
|
|
2
4
|
module Deploy
|
|
3
5
|
class RemoteDependency
|
|
@@ -7,6 +9,7 @@ module Capistrano
|
|
|
7
9
|
def initialize(configuration)
|
|
8
10
|
@configuration = configuration
|
|
9
11
|
@success = true
|
|
12
|
+
@hosts = nil
|
|
10
13
|
end
|
|
11
14
|
|
|
12
15
|
def directory(path, options={})
|
|
@@ -15,6 +18,12 @@ module Capistrano
|
|
|
15
18
|
self
|
|
16
19
|
end
|
|
17
20
|
|
|
21
|
+
def file(path, options={})
|
|
22
|
+
@message ||= "`#{path}' is not a file"
|
|
23
|
+
try("test -f #{path}", options)
|
|
24
|
+
self
|
|
25
|
+
end
|
|
26
|
+
|
|
18
27
|
def writable(path, options={})
|
|
19
28
|
@message ||= "`#{path}' is not writable"
|
|
20
29
|
try("test -w #{path}", options)
|
|
@@ -34,6 +43,48 @@ module Capistrano
|
|
|
34
43
|
self
|
|
35
44
|
end
|
|
36
45
|
|
|
46
|
+
def deb(name, version, options={})
|
|
47
|
+
@message ||= "package `#{name}' #{version} could not be found"
|
|
48
|
+
try("dpkg -s #{name} | grep '^Version: #{version}'", options)
|
|
49
|
+
self
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def rpm(name, version, options={})
|
|
53
|
+
@message ||= "package `#{name}' #{version} could not be found"
|
|
54
|
+
try("rpm -q #{name} | grep '#{version}'", options)
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def match(command, expect, options={})
|
|
59
|
+
expect = Regexp.new(Regexp.escape(expect.to_s)) unless expect.is_a?(Regexp)
|
|
60
|
+
|
|
61
|
+
output_per_server = {}
|
|
62
|
+
try("#{command} ", options) do |ch, stream, out|
|
|
63
|
+
output_per_server[ch[:server]] ||= ''
|
|
64
|
+
output_per_server[ch[:server]] += out
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# It is possible for some of these commands to return a status != 0
|
|
68
|
+
# (for example, rake --version exits with a 1). For this check we
|
|
69
|
+
# just care if the output matches, so we reset the success flag.
|
|
70
|
+
@success = true
|
|
71
|
+
|
|
72
|
+
errored_hosts = []
|
|
73
|
+
output_per_server.each_pair do |server, output|
|
|
74
|
+
next if output =~ expect
|
|
75
|
+
errored_hosts << server
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if errored_hosts.any?
|
|
79
|
+
@hosts = errored_hosts.join(', ')
|
|
80
|
+
output = output_per_server[errored_hosts.first]
|
|
81
|
+
@message = "the output #{output.inspect} from #{command.inspect} did not match #{expect.inspect}"
|
|
82
|
+
@success = false
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
37
88
|
def or(message)
|
|
38
89
|
@message = message
|
|
39
90
|
self
|
|
@@ -45,7 +96,7 @@ module Capistrano
|
|
|
45
96
|
|
|
46
97
|
def message
|
|
47
98
|
s = @message.dup
|
|
48
|
-
s << " (#{@hosts})" if @hosts
|
|
99
|
+
s << " (#{@hosts})" if @hosts
|
|
49
100
|
s
|
|
50
101
|
end
|
|
51
102
|
|
|
@@ -53,8 +104,9 @@ module Capistrano
|
|
|
53
104
|
|
|
54
105
|
def try(command, options)
|
|
55
106
|
return unless @success # short-circuit evaluation
|
|
56
|
-
configuration.
|
|
107
|
+
configuration.invoke_command(command, options) do |ch,stream,out|
|
|
57
108
|
warn "#{ch[:server]}: #{out}" if stream == :err
|
|
109
|
+
yield ch, stream, out if block_given?
|
|
58
110
|
end
|
|
59
111
|
rescue Capistrano::CommandError => e
|
|
60
112
|
@success = false
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
require 'capistrano/recipes/deploy/scm/base'
|
|
2
|
+
require 'rexml/xpath'
|
|
3
|
+
require 'rexml/document'
|
|
4
|
+
|
|
5
|
+
module Capistrano
|
|
6
|
+
module Deploy
|
|
7
|
+
module SCM
|
|
8
|
+
# Accurev bridge for use by Capistrano. This implementation does not
|
|
9
|
+
# implement all features of a Capistrano SCM module. The ones that are
|
|
10
|
+
# left out are either exceedingly difficult to implement with Accurev
|
|
11
|
+
# or are considered bad form.
|
|
12
|
+
#
|
|
13
|
+
# When using this module in a project, the following variables are used:
|
|
14
|
+
# * :repository - This should match the depot that code lives in. If your code
|
|
15
|
+
# exists in a subdirectory, you can append the path depot.
|
|
16
|
+
# eg. foo-depot/bar_dir
|
|
17
|
+
# * :stream - The stream in the depot that code should be pulled from. If
|
|
18
|
+
# left blank, the depot stream will be used
|
|
19
|
+
# * :revision - Should be in the form 'stream/transaction'.
|
|
20
|
+
class Accurev < Base
|
|
21
|
+
include REXML
|
|
22
|
+
default_command 'accurev'
|
|
23
|
+
|
|
24
|
+
# Defines pseudo-revision value for the most recent changes to be deployed.
|
|
25
|
+
def head
|
|
26
|
+
"#{stream}/highest"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Given an Accurev revision identifier, this method returns an identifier that
|
|
30
|
+
# can be used for later SCM calls. This returned identifier will not
|
|
31
|
+
# change as a result of further SCM activity.
|
|
32
|
+
def query_revision(revision)
|
|
33
|
+
internal_revision = InternalRevision.parse(revision)
|
|
34
|
+
return revision unless internal_revision.psuedo_revision?
|
|
35
|
+
|
|
36
|
+
logger.debug("Querying for real revision for #{internal_revision}")
|
|
37
|
+
rev_stream = internal_revision.stream
|
|
38
|
+
|
|
39
|
+
logger.debug("Determining what type of stream #{rev_stream} is...")
|
|
40
|
+
stream_xml = yield show_streams_for(rev_stream)
|
|
41
|
+
stream_doc = Document.new(stream_xml)
|
|
42
|
+
type = XPath.first(stream_doc, '//streams/stream/@type').value
|
|
43
|
+
|
|
44
|
+
case type
|
|
45
|
+
when 'snapshot'
|
|
46
|
+
InternalRevision.new(rev_stream, 'highest').to_s
|
|
47
|
+
else
|
|
48
|
+
logger.debug("Getting latest transaction id in #{rev_stream}")
|
|
49
|
+
# Doing another yield for a second Accurev call. Hopefully this is ok.
|
|
50
|
+
hist_xml = yield scm(:hist, '-ftx', '-s', rev_stream, '-t', 'now.1')
|
|
51
|
+
hist_doc = Document.new(hist_xml)
|
|
52
|
+
transaction_id = XPath.first(hist_doc, '//AcResponse/transaction/@id').value
|
|
53
|
+
InternalRevision.new(stream, transaction_id).to_s
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Pops a copy of the code for the specified Accurev revision identifier.
|
|
58
|
+
# The revision identifier is represented as a stream & transaction ID combo.
|
|
59
|
+
# Accurev can only pop a particular transaction if a stream is created on the server
|
|
60
|
+
# with a time basis of that transaction id. Therefore, we will create a stream with
|
|
61
|
+
# the required criteria and pop that.
|
|
62
|
+
def export(revision_id, destination)
|
|
63
|
+
revision = InternalRevision.parse(revision_id)
|
|
64
|
+
logger.debug("Exporting #{revision.stream}/#{revision.transaction_id} to #{destination}")
|
|
65
|
+
|
|
66
|
+
commands = [
|
|
67
|
+
change_or_create_stream("#{revision.stream}-capistrano-deploy", revision),
|
|
68
|
+
"mkdir -p #{destination}",
|
|
69
|
+
scm_quiet(:pop, "-Rv #{stream}", "-L #{destination}", "'/./#{subdir}'")
|
|
70
|
+
]
|
|
71
|
+
if subdir
|
|
72
|
+
commands.push(
|
|
73
|
+
"mv #{destination}/#{subdir}/* #{destination}",
|
|
74
|
+
"rm -rf #{File.join(destination, subdir)}"
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
commands.join(' && ')
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Returns the command needed to show the changes that exist between the two revisions.
|
|
81
|
+
def log(from, to=head)
|
|
82
|
+
logger.info("Getting transactions between #{from} and #{to}")
|
|
83
|
+
from_rev = InternalRevision.parse(from)
|
|
84
|
+
to_rev = InternalRevision.parse(to)
|
|
85
|
+
|
|
86
|
+
[
|
|
87
|
+
scm(:hist, '-s', from_rev.stream, '-t', "#{to_rev.transaction_id}-#{from_rev.transaction_id}"),
|
|
88
|
+
"sed -e '/transaction #{from_rev.transaction_id}/ { Q }'"
|
|
89
|
+
].join(' | ')
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns the command needed to show the diff between what is deployed and what is
|
|
93
|
+
# pending. Because Accurev can not do this task without creating some streams,
|
|
94
|
+
# two time basis streams will be created for the purposes of doing the diff.
|
|
95
|
+
def diff(from, to=head)
|
|
96
|
+
from = InternalRevision.parse(from)
|
|
97
|
+
to = InternalRevision.parse(to)
|
|
98
|
+
|
|
99
|
+
from_stream = "#{from.stream}-capistrano-diff-from"
|
|
100
|
+
to_stream = "#{to.stream}-capistrano-diff-to"
|
|
101
|
+
|
|
102
|
+
[
|
|
103
|
+
change_or_create_stream(from_stream, from),
|
|
104
|
+
change_or_create_stream(to_stream, to),
|
|
105
|
+
scm(:diff, '-v', from_stream, '-V', to_stream, '-a')
|
|
106
|
+
].join(' && ')
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
def depot
|
|
111
|
+
repository.split('/')[0]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def stream
|
|
115
|
+
variable(:stream) || depot
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def subdir
|
|
119
|
+
repository.split('/')[1..-1].join('/') unless repository.index('/').nil?
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def change_or_create_stream(name, revision)
|
|
123
|
+
[
|
|
124
|
+
scm_quiet(:mkstream, '-b', revision.stream, '-s', name, '-t', revision.transaction_id),
|
|
125
|
+
scm_quiet(:chstream, '-b', revision.stream, '-s', name, '-t', revision.transaction_id)
|
|
126
|
+
].join('; ')
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def show_streams_for(stream)
|
|
130
|
+
scm :show, '-fx', '-s', stream, :streams
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def scm_quiet(*args)
|
|
134
|
+
scm(*args) + (variable(:scm_verbose) ? '' : '&> /dev/null')
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
class InternalRevision
|
|
138
|
+
attr_reader :stream, :transaction_id
|
|
139
|
+
|
|
140
|
+
def self.parse(string)
|
|
141
|
+
match = /([^\/]+)(\/(.+)){0,1}/.match(string)
|
|
142
|
+
raise "Unrecognized revision identifier: #{string}" unless match
|
|
143
|
+
|
|
144
|
+
stream = match[1]
|
|
145
|
+
transaction_id = match[3] || 'highest'
|
|
146
|
+
InternalRevision.new(stream, transaction_id)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def initialize(stream, transaction_id)
|
|
150
|
+
@stream = stream
|
|
151
|
+
@transaction_id = transaction_id
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def psuedo_revision?
|
|
155
|
+
@transaction_id == 'highest'
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def to_s
|
|
159
|
+
"#{stream}/#{transaction_id}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def ==(other)
|
|
163
|
+
(stream == other.stream) && (transaction_id == other.transaction_id)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -54,7 +54,7 @@ module Capistrano
|
|
|
54
54
|
# and "local_scm_command" to be set, if the two differ.
|
|
55
55
|
#
|
|
56
56
|
# Alternatively, it may be called with a block, and for the duration of
|
|
57
|
-
# the block, all requests on this configuration object will be
|
|
57
|
+
# the block, all requests on this configuration object will be
|
|
58
58
|
# considered local.
|
|
59
59
|
def local
|
|
60
60
|
if block_given?
|
|
@@ -114,6 +114,18 @@ module Capistrano
|
|
|
114
114
|
raise NotImplementedError, "`query_revision' is not implemented by #{self.class.name}"
|
|
115
115
|
end
|
|
116
116
|
|
|
117
|
+
# Returns the revision number immediately following revision, if at
|
|
118
|
+
# all possible. A block should always be passed to this method, which
|
|
119
|
+
# accepts a command to invoke and returns the result, although a
|
|
120
|
+
# particular SCM's implementation is not required to invoke the block.
|
|
121
|
+
#
|
|
122
|
+
# By default, this method simply returns the revision itself. If a
|
|
123
|
+
# particular SCM is able to determine a subsequent revision given a
|
|
124
|
+
# revision identifier, it should override this method.
|
|
125
|
+
def next_revision(revision)
|
|
126
|
+
revision
|
|
127
|
+
end
|
|
128
|
+
|
|
117
129
|
# Should analyze the given text and determine whether or not a
|
|
118
130
|
# response is expected, and if so, return the appropriate response.
|
|
119
131
|
# If no response is expected, return nil. The +state+ parameter is a
|
|
@@ -138,15 +150,22 @@ module Capistrano
|
|
|
138
150
|
command || default_command
|
|
139
151
|
end
|
|
140
152
|
|
|
153
|
+
# A helper method that can be used to define SCM commands naturally.
|
|
154
|
+
# It returns a single string with all arguments joined by spaces,
|
|
155
|
+
# with the scm command prefixed onto it.
|
|
156
|
+
def scm(*args)
|
|
157
|
+
[command, *args].compact.join(" ")
|
|
158
|
+
end
|
|
159
|
+
|
|
141
160
|
private
|
|
142
161
|
|
|
143
162
|
# A helper for accessing variable values, which takes into
|
|
144
163
|
# consideration the current mode ("normal" vs. "local").
|
|
145
|
-
def variable(name)
|
|
164
|
+
def variable(name, default = nil)
|
|
146
165
|
if local? && configuration.exists?("local_#{name}".to_sym)
|
|
147
|
-
return configuration["local_#{name}".to_sym]
|
|
166
|
+
return configuration["local_#{name}".to_sym].nil? ? default : configuration["local_#{name}".to_sym]
|
|
148
167
|
else
|
|
149
|
-
configuration[name]
|
|
168
|
+
configuration[name].nil? ? default : configuration[name]
|
|
150
169
|
end
|
|
151
170
|
end
|
|
152
171
|
|
|
@@ -162,17 +181,18 @@ module Capistrano
|
|
|
162
181
|
self.class.default_command
|
|
163
182
|
end
|
|
164
183
|
|
|
165
|
-
# A helper method that can be used to define SCM commands naturally.
|
|
166
|
-
# It returns a single string with all arguments joined by spaces,
|
|
167
|
-
# with the scm command prefixed onto it.
|
|
168
|
-
def scm(*args)
|
|
169
|
-
[command, *args].compact.join(" ")
|
|
170
|
-
end
|
|
171
|
-
|
|
172
184
|
# A convenience method for accessing the declared repository value.
|
|
173
185
|
def repository
|
|
174
186
|
variable(:repository)
|
|
175
187
|
end
|
|
188
|
+
|
|
189
|
+
def arguments(command = :all)
|
|
190
|
+
value = variable(:scm_arguments)
|
|
191
|
+
if value.is_a?(Hash)
|
|
192
|
+
value = value[command]
|
|
193
|
+
end
|
|
194
|
+
value
|
|
195
|
+
end
|
|
176
196
|
end
|
|
177
197
|
|
|
178
198
|
end
|
|
@@ -21,7 +21,7 @@ module Capistrano
|
|
|
21
21
|
# Returns the command that will check out the given revision to the
|
|
22
22
|
# given destination.
|
|
23
23
|
def checkout(revision, destination)
|
|
24
|
-
scm :
|
|
24
|
+
scm :checkout, "--lightweight", revswitch(revision), repository, destination
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# The bzr 'update' command does not support updating to a specific
|
|
@@ -33,13 +33,9 @@ module Capistrano
|
|
|
33
33
|
commands.join(" && ")
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
# The bzr 'export'
|
|
37
|
-
# specify the repository itself, so it only works if there is a working
|
|
38
|
-
# tree handy, locally. Since this needs to work even on a remote host
|
|
39
|
-
# where there is no working tree, we'll just do a checkout, followed
|
|
40
|
-
# by a deletion of the ".bzr" metadata directory.
|
|
36
|
+
# The bzr 'export' does an export similar to other SCM systems
|
|
41
37
|
def export(revision, destination)
|
|
42
|
-
|
|
38
|
+
scm :export, revswitch(revision), destination, repository
|
|
43
39
|
end
|
|
44
40
|
|
|
45
41
|
# The bzr "diff" command doesn't accept a repository argument, so it
|
|
@@ -63,20 +59,24 @@ module Capistrano
|
|
|
63
59
|
# If the 'revision' argument, on the other hand, is not :head, it is
|
|
64
60
|
# simply returned.
|
|
65
61
|
def query_revision(revision)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
return revision unless :head == revision
|
|
63
|
+
|
|
64
|
+
command = scm('revno', repository)
|
|
65
|
+
result = yield(command)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Increments the given revision number and returns it.
|
|
69
|
+
def next_revision(revision)
|
|
70
|
+
revision.to_i + 1
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
private
|
|
74
74
|
|
|
75
75
|
def revswitch(revision)
|
|
76
|
-
if revision == :head
|
|
76
|
+
if revision == :head || revision.nil?
|
|
77
77
|
nil
|
|
78
78
|
else
|
|
79
|
-
"-r #{revision}"
|
|
79
|
+
"-r #{revision}".chomp
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
82
|
end
|