minestrone 0.0.1
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/.github/workflows/main.yml +32 -0
- data/.gitignore +5 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +35 -0
- data/Rakefile +10 -0
- data/bin/capify +89 -0
- data/bin/min +5 -0
- data/docs/lib-codebase-map.md +162 -0
- data/docs/lib-dependency-graph.svg +129 -0
- data/lib/minestrone/callback.rb +45 -0
- data/lib/minestrone/cli/help.rb +131 -0
- data/lib/minestrone/cli/help.txt +72 -0
- data/lib/minestrone/cli/options.rb +232 -0
- data/lib/minestrone/cli.rb +159 -0
- data/lib/minestrone/command.rb +177 -0
- data/lib/minestrone/configuration/actions/file_transfer.rb +53 -0
- data/lib/minestrone/configuration/actions/inspect.rb +46 -0
- data/lib/minestrone/configuration/actions/invocation.rb +202 -0
- data/lib/minestrone/configuration/alias_task.rb +29 -0
- data/lib/minestrone/configuration/callbacks.rb +129 -0
- data/lib/minestrone/configuration/connections.rb +66 -0
- data/lib/minestrone/configuration/execution.rb +139 -0
- data/lib/minestrone/configuration/loading.rb +207 -0
- data/lib/minestrone/configuration/log_formatters.rb +75 -0
- data/lib/minestrone/configuration/namespaces.rb +225 -0
- data/lib/minestrone/configuration/servers.rb +70 -0
- data/lib/minestrone/configuration/variables.rb +115 -0
- data/lib/minestrone/configuration.rb +69 -0
- data/lib/minestrone/errors.rb +17 -0
- data/lib/minestrone/ext/string.rb +7 -0
- data/lib/minestrone/extensions.rb +56 -0
- data/lib/minestrone/logger.rb +171 -0
- data/lib/minestrone/processable.rb +50 -0
- data/lib/minestrone/recipes/deploy/assets.rb +194 -0
- data/lib/minestrone/recipes/deploy/bundler.rb +81 -0
- data/lib/minestrone/recipes/deploy/dependencies.rb +44 -0
- data/lib/minestrone/recipes/deploy/local_dependency.rb +45 -0
- data/lib/minestrone/recipes/deploy/remote_dependency.rb +119 -0
- data/lib/minestrone/recipes/deploy/scm/base.rb +204 -0
- data/lib/minestrone/recipes/deploy/scm/git.rb +284 -0
- data/lib/minestrone/recipes/deploy/scm/none.rb +54 -0
- data/lib/minestrone/recipes/deploy/scm.rb +22 -0
- data/lib/minestrone/recipes/deploy/strategy/base.rb +87 -0
- data/lib/minestrone/recipes/deploy/strategy/copy.rb +353 -0
- data/lib/minestrone/recipes/deploy/strategy/remote_cache.rb +80 -0
- data/lib/minestrone/recipes/deploy/strategy.rb +22 -0
- data/lib/minestrone/recipes/deploy.rb +639 -0
- data/lib/minestrone/recipes/standard.rb +23 -0
- data/lib/minestrone/recipes/templates/maintenance.rhtml +53 -0
- data/lib/minestrone/server_definition.rb +56 -0
- data/lib/minestrone/ssh.rb +81 -0
- data/lib/minestrone/task_definition.rb +82 -0
- data/lib/minestrone/transfer.rb +205 -0
- data/lib/minestrone/version.rb +11 -0
- data/lib/minestrone.rb +3 -0
- data/minestrone.gemspec +32 -0
- data/test/cli/execute_test.rb +130 -0
- data/test/cli/help_test.rb +178 -0
- data/test/cli/options_test.rb +315 -0
- data/test/cli/ui_test.rb +26 -0
- data/test/cli_test.rb +17 -0
- data/test/command_test.rb +305 -0
- data/test/configuration/actions/file_transfer_test.rb +61 -0
- data/test/configuration/actions/inspect_test.rb +76 -0
- data/test/configuration/actions/invocation_test.rb +258 -0
- data/test/configuration/alias_task_test.rb +110 -0
- data/test/configuration/callbacks_test.rb +201 -0
- data/test/configuration/connections_test.rb +192 -0
- data/test/configuration/execution_test.rb +176 -0
- data/test/configuration/loading_test.rb +149 -0
- data/test/configuration/namespace_dsl_test.rb +325 -0
- data/test/configuration/servers_test.rb +100 -0
- data/test/configuration/variables_test.rb +191 -0
- data/test/configuration_test.rb +77 -0
- data/test/deploy/local_dependency_test.rb +61 -0
- data/test/deploy/remote_dependency_test.rb +146 -0
- data/test/deploy/scm/base_test.rb +55 -0
- data/test/deploy/scm/git_test.rb +260 -0
- data/test/deploy/scm/none_test.rb +26 -0
- data/test/deploy/strategy/copy_test.rb +360 -0
- data/test/extensions_test.rb +69 -0
- data/test/fixtures/cli_integration.rb +5 -0
- data/test/fixtures/config.rb +4 -0
- data/test/fixtures/custom.rb +3 -0
- data/test/logger_formatting_test.rb +149 -0
- data/test/logger_test.rb +134 -0
- data/test/recipes_test.rb +26 -0
- data/test/server_definition_test.rb +121 -0
- data/test/ssh_test.rb +99 -0
- data/test/task_definition_test.rb +117 -0
- data/test/transfer_test.rb +172 -0
- data/test/utils.rb +28 -0
- data/test/version_test.rb +11 -0
- metadata +258 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
load 'deploy' unless defined?(_cset)
|
|
6
|
+
|
|
7
|
+
_cset :asset_env, "RAILS_GROUPS=assets"
|
|
8
|
+
_cset :assets_prefix, "assets"
|
|
9
|
+
_cset :shared_assets_prefix, "assets"
|
|
10
|
+
_cset :expire_assets_after, (3600 * 24 * 7)
|
|
11
|
+
_cset(:asset_manifest_prefix) { (`sprockets -v`.chomp < "3.0" ? "manifest" : ".sprockets-manifest") rescue "manifest" }
|
|
12
|
+
|
|
13
|
+
_cset :normalize_asset_timestamps, false
|
|
14
|
+
|
|
15
|
+
before 'deploy:finalize_update', 'deploy:assets:symlink'
|
|
16
|
+
after 'deploy:update_code', 'deploy:assets:precompile'
|
|
17
|
+
before 'deploy:assets:precompile', 'deploy:assets:update_asset_mtimes'
|
|
18
|
+
after 'deploy:cleanup', 'deploy:assets:clean_expired'
|
|
19
|
+
after 'deploy:rollback:revision', 'deploy:assets:rollback'
|
|
20
|
+
|
|
21
|
+
def shared_manifest_path
|
|
22
|
+
@shared_manifest_path ||= capture("ls #{shared_path.shellescape}/#{shared_assets_prefix}/#{asset_manifest_prefix}*").strip
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Parses manifest and returns array of uncompressed and compressed asset filenames with and without digests
|
|
26
|
+
# "Intelligently" determines format of string - supports YAML and JSON
|
|
27
|
+
def parse_manifest(str)
|
|
28
|
+
assets_hash = str[0,1] == '{' ? JSON.parse(str)['assets'] : YAML.load(str)
|
|
29
|
+
|
|
30
|
+
assets_hash.to_a.flatten.map {|a| [a, "#{a}.gz"] }.flatten
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
namespace :deploy do
|
|
34
|
+
namespace :assets do
|
|
35
|
+
desc <<-DESC
|
|
36
|
+
[internal] This task will set up a symlink to the shared directory \
|
|
37
|
+
for the assets directory. Assets are shared across deploys to avoid \
|
|
38
|
+
mid-deploy mismatches between old application html asking for assets \
|
|
39
|
+
and getting a 404 file not found error. The assets cache is shared \
|
|
40
|
+
for efficiency. If you customize the assets path prefix, override the \
|
|
41
|
+
:assets_prefix variable to match. If you customize shared assets path \
|
|
42
|
+
prefix, override :shared_assets_prefix variable to match.
|
|
43
|
+
DESC
|
|
44
|
+
task :symlink do
|
|
45
|
+
run <<-CMD.compact
|
|
46
|
+
rm -rf #{latest_release}/public/#{assets_prefix} &&
|
|
47
|
+
mkdir -p #{latest_release}/public &&
|
|
48
|
+
mkdir -p #{shared_path}/#{shared_assets_prefix} &&
|
|
49
|
+
ln -s #{shared_path}/#{shared_assets_prefix} #{latest_release}/public/#{assets_prefix}
|
|
50
|
+
CMD
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
desc <<-DESC
|
|
54
|
+
Run the asset precompilation rake task. You can specify the full path \
|
|
55
|
+
to the rake executable by setting the rake variable. You can also \
|
|
56
|
+
specify additional environment variables to pass to rake via the \
|
|
57
|
+
asset_env variable. The defaults are:
|
|
58
|
+
|
|
59
|
+
set :rake, "rake"
|
|
60
|
+
set :rails_env, "production"
|
|
61
|
+
set :asset_env, "RAILS_GROUPS=assets"
|
|
62
|
+
DESC
|
|
63
|
+
task :precompile do
|
|
64
|
+
run <<-CMD.compact
|
|
65
|
+
cd -- #{latest_release} &&
|
|
66
|
+
RAILS_ENV=#{rails_env.to_s.shellescape} #{asset_env} #{rake} assets:precompile
|
|
67
|
+
CMD
|
|
68
|
+
|
|
69
|
+
if capture("ls -1 #{shared_path.shellescape}/#{shared_assets_prefix}/#{asset_manifest_prefix}* | wc -l").to_i > 1
|
|
70
|
+
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/minestrone/minestrone/wiki/Upgrading-to-Rails-4#asset-pipeline"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Sync manifest filenames if our manifest has a random filename
|
|
74
|
+
if shared_manifest_path =~ /#{asset_manifest_prefix}-.+\./
|
|
75
|
+
run <<-CMD.compact
|
|
76
|
+
[ -e #{shared_manifest_path.shellescape} ] || mv -- #{shared_path.shellescape}/#{shared_assets_prefix}/#{asset_manifest_prefix}* #{shared_manifest_path.shellescape}
|
|
77
|
+
CMD
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Copy manifest to release root (for clean_expired task)
|
|
81
|
+
run <<-CMD.compact
|
|
82
|
+
cp -- #{shared_manifest_path.shellescape} #{current_release.to_s.shellescape}/assets_manifest#{File.extname(shared_manifest_path)}
|
|
83
|
+
CMD
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
desc <<-DESC
|
|
87
|
+
[internal] Updates the mtimes for assets that are required by the current release.
|
|
88
|
+
This task runs before assets:precompile.
|
|
89
|
+
DESC
|
|
90
|
+
task :update_asset_mtimes do
|
|
91
|
+
# Fetch assets/manifest contents.
|
|
92
|
+
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
|
|
93
|
+
|
|
94
|
+
if manifest_content != ""
|
|
95
|
+
current_assets = parse_manifest(manifest_content)
|
|
96
|
+
logger.info "Updating mtimes for ~#{current_assets.count} assets..."
|
|
97
|
+
put current_assets.map{|a| "#{shared_path}/#{shared_assets_prefix}/#{a}" }.join("\n"), "#{deploy_to}/TOUCH_ASSETS", :via => :scp
|
|
98
|
+
run <<-CMD.compact
|
|
99
|
+
cat #{deploy_to.shellescape}/TOUCH_ASSETS | while read asset; do
|
|
100
|
+
touch -c -- "$asset";
|
|
101
|
+
done &&
|
|
102
|
+
rm -f -- #{deploy_to.shellescape}/TOUCH_ASSETS
|
|
103
|
+
CMD
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
desc <<-DESC
|
|
108
|
+
Run the asset clean rake task. Use with caution, this will delete \
|
|
109
|
+
all of your compiled assets. You can specify the full path \
|
|
110
|
+
to the rake executable by setting the rake variable. You can also \
|
|
111
|
+
specify additional environment variables to pass to rake via the \
|
|
112
|
+
asset_env variable. The defaults are:
|
|
113
|
+
|
|
114
|
+
set :rake, "rake"
|
|
115
|
+
set :rails_env, "production"
|
|
116
|
+
set :asset_env, "RAILS_GROUPS=assets"
|
|
117
|
+
DESC
|
|
118
|
+
task :clean do
|
|
119
|
+
run "cd #{latest_release} && #{rake} RAILS_ENV=#{rails_env} #{asset_env} assets:clean"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
desc <<-DESC
|
|
123
|
+
Clean up any assets that haven't been deployed for more than :expire_assets_after seconds.
|
|
124
|
+
Default time to keep old assets is one week. Set the :expire_assets_after variable
|
|
125
|
+
to change the assets expiry time. Assets will only be deleted if they are not required by
|
|
126
|
+
an existing release.
|
|
127
|
+
DESC
|
|
128
|
+
task :clean_expired do
|
|
129
|
+
# Fetch all assets_manifest contents.
|
|
130
|
+
manifests_output = capture <<-CMD.compact
|
|
131
|
+
for manifest in #{releases_path.shellescape}/*/assets_manifest.*; do
|
|
132
|
+
cat -- "$manifest" 2> /dev/null && printf ':::' || true;
|
|
133
|
+
done
|
|
134
|
+
CMD
|
|
135
|
+
manifests = manifests_output.split(':::')
|
|
136
|
+
|
|
137
|
+
if manifests.empty?
|
|
138
|
+
logger.info "No manifests in #{releases_path}/*/assets_manifest.*"
|
|
139
|
+
else
|
|
140
|
+
logger.info "Fetched #{manifests.count} manifests from #{releases_path}/*/assets_manifest.*"
|
|
141
|
+
current_assets = Set.new
|
|
142
|
+
manifests.each do |content|
|
|
143
|
+
current_assets += parse_manifest(content)
|
|
144
|
+
end
|
|
145
|
+
current_assets += [File.basename(shared_manifest_path), "sources_manifest.yml"]
|
|
146
|
+
|
|
147
|
+
# Write the list of required assets to server.
|
|
148
|
+
logger.info "Writing required assets to #{deploy_to}/REQUIRED_ASSETS..."
|
|
149
|
+
escaped_assets = current_assets.sort.join("\n").gsub("\"", "\\\"") << "\n"
|
|
150
|
+
put escaped_assets, "#{deploy_to}/REQUIRED_ASSETS", :via => :scp
|
|
151
|
+
|
|
152
|
+
# Finds all files older than X minutes, then removes them if they are not referenced
|
|
153
|
+
# in REQUIRED_ASSETS.
|
|
154
|
+
expire_after_mins = (expire_assets_after.to_f / 60.0).to_i
|
|
155
|
+
logger.info "Removing assets that haven't been deployed for #{expire_after_mins} minutes..."
|
|
156
|
+
# LC_COLLATE=C tells the `sort` and `comm` commands to sort files in byte order.
|
|
157
|
+
run <<-CMD.compact
|
|
158
|
+
cd -- #{deploy_to.shellescape}/ &&
|
|
159
|
+
LC_COLLATE=C sort REQUIRED_ASSETS -o REQUIRED_ASSETS &&
|
|
160
|
+
cd -- #{shared_path.shellescape}/#{shared_assets_prefix}/ &&
|
|
161
|
+
for f in $(
|
|
162
|
+
find * -mmin +#{expire_after_mins.to_s.shellescape} -type f | LC_COLLATE=C sort |
|
|
163
|
+
LC_COLLATE=C comm -23 -- - #{deploy_to.shellescape}/REQUIRED_ASSETS
|
|
164
|
+
); do
|
|
165
|
+
echo "Removing unneeded asset: $f";
|
|
166
|
+
rm -f -- "$f";
|
|
167
|
+
done;
|
|
168
|
+
rm -f -- #{deploy_to.shellescape}/REQUIRED_ASSETS
|
|
169
|
+
CMD
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
desc <<-DESC
|
|
174
|
+
Rolls back assets to the previous release by symlinking the release's manifest
|
|
175
|
+
to shared/assets/manifest, and finally recompiling or regenerating nondigest assets.
|
|
176
|
+
DESC
|
|
177
|
+
task :rollback do
|
|
178
|
+
previous_manifest = capture("ls #{previous_release.shellescape}/assets_manifest.*").strip
|
|
179
|
+
|
|
180
|
+
if capture("[ -e #{previous_manifest.shellescape} ] && echo true || echo false").strip != 'true'
|
|
181
|
+
puts "#{previous_manifest} is missing! Cannot roll back assets. " <<
|
|
182
|
+
"Please run deploy:assets:precompile to update your assets when the rollback is finished."
|
|
183
|
+
else
|
|
184
|
+
restored_manifest_path = shared_manifest_path
|
|
185
|
+
|
|
186
|
+
run <<-CMD.compact
|
|
187
|
+
cd -- #{previous_release.shellescape} &&
|
|
188
|
+
cp -f -- #{previous_manifest.shellescape} #{restored_manifest_path.shellescape} &&
|
|
189
|
+
[ -z "$(#{rake} -P | grep assets:precompile:nondigest)" ] || #{rake} RAILS_ENV=#{rails_env.to_s.shellescape} #{asset_env} assets:precompile:nondigest
|
|
190
|
+
CMD
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Based on:
|
|
4
|
+
# https://github.com/ruby/rubygems/blob/master/bundler/lib/bundler/minestrone.rb
|
|
5
|
+
# and https://github.com/minestrone/bundler
|
|
6
|
+
|
|
7
|
+
load 'deploy' unless defined?(_cset)
|
|
8
|
+
|
|
9
|
+
_cset :bundle_env, ''
|
|
10
|
+
_cset :bundle_cmd, 'bundle'
|
|
11
|
+
_cset(:bundle_path) { "#{shared_path}/bundle" }
|
|
12
|
+
_cset :bundle_without, [:development, :test]
|
|
13
|
+
_cset :bundle_flags, '--quiet'
|
|
14
|
+
|
|
15
|
+
set(:rake) { "#{bundle_cmd} exec rake" }
|
|
16
|
+
|
|
17
|
+
before "deploy:finalize_update", "bundle:install"
|
|
18
|
+
before "bundle:install", "bundle:config"
|
|
19
|
+
|
|
20
|
+
namespace :bundle do
|
|
21
|
+
desc <<-DESC
|
|
22
|
+
Sets up the Bundler configuration appropriate for a production environment.
|
|
23
|
+
You can customize the settings using the following variables:
|
|
24
|
+
|
|
25
|
+
set :bundle_cmd, 'bundle' # e.g. '/usr/local/bin/bundle
|
|
26
|
+
set(:bundle_path) { shared_path + "/bundle" }
|
|
27
|
+
set :bundle_without, [:development, :test]
|
|
28
|
+
DESC
|
|
29
|
+
|
|
30
|
+
task :config do
|
|
31
|
+
bundle_cmd = fetch(:bundle_cmd)
|
|
32
|
+
bundle_path = fetch(:bundle_path)
|
|
33
|
+
|
|
34
|
+
without = fetch(:bundle_without)
|
|
35
|
+
without = [without] unless without.is_a?(Array)
|
|
36
|
+
|
|
37
|
+
settings = [
|
|
38
|
+
['deployment', 'true'],
|
|
39
|
+
['path', bundle_path],
|
|
40
|
+
['without', without.map(&:to_s).join(' ')],
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
settings.each do |key, value|
|
|
44
|
+
run "cd #{release_path} && #{bundle_cmd} config set --local #{key} '#{value}'"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
desc <<-DESC
|
|
49
|
+
Installs the gems required by the app. By default, gems are installed to
|
|
50
|
+
\"\#{shared_path}/bundle\", with :development and :test groups skipped.
|
|
51
|
+
|
|
52
|
+
You can customize the settings using the following variables:
|
|
53
|
+
|
|
54
|
+
set :bundle_env, '' # e.g. 'SOME_LIBRARY_PATH=/usr/local'
|
|
55
|
+
set :bundle_cmd, 'bundle' # e.g. '/usr/local/bin/bundle
|
|
56
|
+
set(:bundle_path) { shared_path + "/bundle" }
|
|
57
|
+
set :bundle_without, [:development, :test]
|
|
58
|
+
set :bundle_flags, '--quiet'
|
|
59
|
+
DESC
|
|
60
|
+
|
|
61
|
+
task :install do
|
|
62
|
+
bundle_env = fetch(:bundle_env)
|
|
63
|
+
bundle_cmd = fetch(:bundle_cmd)
|
|
64
|
+
bundle_flags = fetch(:bundle_flags)
|
|
65
|
+
|
|
66
|
+
bundle_env += " " unless bundle_env.to_s.empty?
|
|
67
|
+
|
|
68
|
+
run "cd #{release_path} && #{bundle_env}#{bundle_cmd} install #{bundle_flags}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
desc <<-DESC
|
|
72
|
+
Cleans up older versions of gems in the shared bundle folder. This removes all
|
|
73
|
+
gems that aren't currently referenced by the Gemfile.lock.
|
|
74
|
+
DESC
|
|
75
|
+
|
|
76
|
+
task :clean do
|
|
77
|
+
bundle_cmd = fetch(:bundle_cmd, "bundle")
|
|
78
|
+
|
|
79
|
+
run "cd #{current_path} && #{bundle_cmd} clean"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require 'minestrone/recipes/deploy/local_dependency'
|
|
2
|
+
require 'minestrone/recipes/deploy/remote_dependency'
|
|
3
|
+
|
|
4
|
+
module Minestrone
|
|
5
|
+
module Deploy
|
|
6
|
+
class Dependencies
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
attr_reader :configuration
|
|
10
|
+
|
|
11
|
+
def initialize(configuration)
|
|
12
|
+
@configuration = configuration
|
|
13
|
+
@dependencies = []
|
|
14
|
+
yield self if block_given?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def check
|
|
18
|
+
yield self
|
|
19
|
+
self
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def remote
|
|
23
|
+
dep = RemoteDependency.new(configuration)
|
|
24
|
+
@dependencies << dep
|
|
25
|
+
dep
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def local
|
|
29
|
+
dep = LocalDependency.new(configuration)
|
|
30
|
+
@dependencies << dep
|
|
31
|
+
dep
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def each
|
|
35
|
+
@dependencies.each { |d| yield d }
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def pass?
|
|
40
|
+
all? { |d| d.pass? }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Minestrone
|
|
4
|
+
module Deploy
|
|
5
|
+
class LocalDependency
|
|
6
|
+
attr_reader :configuration
|
|
7
|
+
attr_reader :message
|
|
8
|
+
|
|
9
|
+
def initialize(configuration)
|
|
10
|
+
@configuration = configuration
|
|
11
|
+
@success = true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def command(command)
|
|
15
|
+
@message ||= "`#{command}' could not be found in the path on the local host"
|
|
16
|
+
@success = find_in_path(command)
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def or(message)
|
|
21
|
+
@message = message
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def pass?
|
|
26
|
+
@success
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# Searches the path, looking for the given utility. If an executable
|
|
32
|
+
# file is found that matches the parameter, this returns true.
|
|
33
|
+
def find_in_path(utility)
|
|
34
|
+
path = (ENV['PATH'] || "").split(File::PATH_SEPARATOR)
|
|
35
|
+
|
|
36
|
+
path.each do |dir|
|
|
37
|
+
file = File.join(dir, utility)
|
|
38
|
+
return true if File.executable?(file)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
false
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minestrone/errors'
|
|
4
|
+
|
|
5
|
+
module Minestrone
|
|
6
|
+
module Deploy
|
|
7
|
+
class RemoteDependency
|
|
8
|
+
attr_reader :configuration
|
|
9
|
+
attr_reader :hosts
|
|
10
|
+
|
|
11
|
+
def initialize(configuration)
|
|
12
|
+
@configuration = configuration
|
|
13
|
+
@success = true
|
|
14
|
+
@hosts = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def directory(path, options = {})
|
|
18
|
+
@message ||= "`#{path}' is not a directory"
|
|
19
|
+
try("test -d #{path}", options)
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def file(path, options = {})
|
|
24
|
+
@message ||= "`#{path}' is not a file"
|
|
25
|
+
try("test -f #{path}", options)
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def writable(path, options = {})
|
|
30
|
+
@message ||= "`#{path}' is not writable"
|
|
31
|
+
try("test -w #{path}", options)
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def command(command, options = {})
|
|
36
|
+
@message ||= "`#{command}' could not be found in the path"
|
|
37
|
+
try("which #{command}", options)
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def gem(name, version, options = {})
|
|
42
|
+
@message ||= "gem `#{name}' #{version} could not be found"
|
|
43
|
+
gem_cmd = configuration.fetch(:gem_command, "gem")
|
|
44
|
+
try("#{gem_cmd} specification --version '#{version}' #{name} 2>&1 | awk 'BEGIN { s = 0 } /^name:/ { s = 1; exit }; END { if(s == 0) exit 1 }'", options)
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def deb(name, version, options = {})
|
|
49
|
+
@message ||= "package `#{name}' #{version} could not be found"
|
|
50
|
+
try("dpkg -s #{name} | grep '^Version: #{version}'", options)
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def rpm(name, version, options = {})
|
|
55
|
+
@message ||= "package `#{name}' #{version} could not be found"
|
|
56
|
+
try("rpm -q #{name} | grep '#{version}'", options)
|
|
57
|
+
self
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def match(command, expect, options = {})
|
|
61
|
+
expect = Regexp.new(Regexp.escape(expect.to_s)) unless expect.is_a?(Regexp)
|
|
62
|
+
|
|
63
|
+
output_per_server = {}
|
|
64
|
+
try("#{command} ", options) do |ch, stream, out|
|
|
65
|
+
output_per_server[ch[:server]] ||= ''
|
|
66
|
+
output_per_server[ch[:server]] += out
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# It is possible for some of these commands to return a status != 0
|
|
70
|
+
# (for example, rake --version exits with a 1). For this check we
|
|
71
|
+
# just care if the output matches, so we reset the success flag.
|
|
72
|
+
@success = true
|
|
73
|
+
|
|
74
|
+
errored_hosts = []
|
|
75
|
+
output_per_server.each_pair do |server, output|
|
|
76
|
+
next if output =~ expect
|
|
77
|
+
errored_hosts << server
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if errored_hosts.any?
|
|
81
|
+
@hosts = errored_hosts.join(', ')
|
|
82
|
+
output = output_per_server[errored_hosts.first]
|
|
83
|
+
@message = "the output #{output.inspect} from #{command.inspect} did not match #{expect.inspect}"
|
|
84
|
+
@success = false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
self
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def or(message)
|
|
91
|
+
@message = message
|
|
92
|
+
self
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def pass?
|
|
96
|
+
@success
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def message
|
|
100
|
+
s = @message.dup
|
|
101
|
+
s << " (#{@hosts})" if @hosts
|
|
102
|
+
s
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def try(command, options)
|
|
108
|
+
return unless @success # short-circuit evaluation
|
|
109
|
+
configuration.invoke_command(command, options) do |ch,stream,out|
|
|
110
|
+
warn "#{ch[:server]}: #{out}" if stream == :err
|
|
111
|
+
yield ch, stream, out if block_given?
|
|
112
|
+
end
|
|
113
|
+
rescue Minestrone::CommandError => e
|
|
114
|
+
@success = false
|
|
115
|
+
@hosts = e.host.to_s
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Minestrone
|
|
4
|
+
module Deploy
|
|
5
|
+
module SCM
|
|
6
|
+
|
|
7
|
+
# The ancestor class for all Minestrone SCM implementations. It provides
|
|
8
|
+
# minimal infrastructure for subclasses to build upon and override.
|
|
9
|
+
#
|
|
10
|
+
# Note that subclasses that implement this abstract class only return
|
|
11
|
+
# the commands that need to be executed--they do not execute the commands
|
|
12
|
+
# themselves. In this way, the deployment method may execute the commands
|
|
13
|
+
# either locally or remotely, as necessary.
|
|
14
|
+
|
|
15
|
+
class Base
|
|
16
|
+
class << self
|
|
17
|
+
# If no parameters are given, it returns the current configured
|
|
18
|
+
# name of the command-line utility of this SCM. If a parameter is
|
|
19
|
+
# given, the defeault command is set to that value.
|
|
20
|
+
def default_command(value = nil)
|
|
21
|
+
if value
|
|
22
|
+
@default_command = value
|
|
23
|
+
else
|
|
24
|
+
@default_command
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Wraps an SCM instance and forces all messages sent to it to be
|
|
30
|
+
# relayed to the underlying SCM instance, in "local" mode. See
|
|
31
|
+
# Base#local.
|
|
32
|
+
class LocalProxy
|
|
33
|
+
def initialize(scm)
|
|
34
|
+
@scm = scm
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def method_missing(sym, *args, &block)
|
|
38
|
+
@scm.local { return @scm.send(sym, *args, &block) }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The options available for this SCM instance to reference. Should be
|
|
43
|
+
# treated like a hash.
|
|
44
|
+
attr_reader :configuration
|
|
45
|
+
|
|
46
|
+
# Creates a new SCM instance with the given configuration options.
|
|
47
|
+
def initialize(configuration = {})
|
|
48
|
+
@configuration = configuration
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Returns a proxy that wraps the SCM instance and forces it to operate
|
|
52
|
+
# in "local" mode, which changes how variables are looked up in the
|
|
53
|
+
# configuration. Normally, if the value of a variable "foo" is needed,
|
|
54
|
+
# it is queried for in the configuration as "foo". However, in "local"
|
|
55
|
+
# mode, first "local_foo" would be looked for, and only if it is not
|
|
56
|
+
# found would "foo" be used. This allows for both (e.g.) "scm_command"
|
|
57
|
+
# and "local_scm_command" to be set, if the two differ.
|
|
58
|
+
#
|
|
59
|
+
# Alternatively, it may be called with a block, and for the duration of
|
|
60
|
+
# the block, all requests on this configuration object will be
|
|
61
|
+
# considered local.
|
|
62
|
+
def local
|
|
63
|
+
if block_given?
|
|
64
|
+
begin
|
|
65
|
+
saved, @local_mode = @local_mode, true
|
|
66
|
+
yield
|
|
67
|
+
ensure
|
|
68
|
+
@local_mode = saved
|
|
69
|
+
end
|
|
70
|
+
else
|
|
71
|
+
LocalProxy.new(self)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Returns true if running in "local" mode. See #local.
|
|
76
|
+
def local?
|
|
77
|
+
@local_mode
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Returns the string used to identify the latest revision in the
|
|
81
|
+
# repository. This will be passed as the "revision" parameter of
|
|
82
|
+
# the methods below.
|
|
83
|
+
def head
|
|
84
|
+
raise NotImplementedError, "`head' is not implemented by #{self.class.name}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Checkout a copy of the repository, at the given +revision+, to the
|
|
88
|
+
# given +destination+. The checkout is suitable for doing development
|
|
89
|
+
# work in, e.g. allowing subsequent commits and updates.
|
|
90
|
+
def checkout(revision, destination)
|
|
91
|
+
raise NotImplementedError, "`checkout' is not implemented by #{self.class.name}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Resynchronize the working copy in +destination+ to the specified
|
|
95
|
+
# +revision+.
|
|
96
|
+
def sync(revision, destination)
|
|
97
|
+
raise NotImplementedError, "`sync' is not implemented by #{self.class.name}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Compute the difference between the two revisions, +from+ and +to+.
|
|
101
|
+
def diff(from, to = nil)
|
|
102
|
+
raise NotImplementedError, "`diff' is not implemented by #{self.class.name}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Return a log of all changes between the two specified revisions,
|
|
106
|
+
# +from+ and +to+, inclusive.
|
|
107
|
+
def log(from, to = nil)
|
|
108
|
+
raise NotImplementedError, "`log' is not implemented by #{self.class.name}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# If the given revision represents a "real" revision, this should
|
|
112
|
+
# simply return the revision value. If it represends a pseudo-revision
|
|
113
|
+
# (like Subversions "HEAD" identifier), it should yield a string
|
|
114
|
+
# containing the commands that, when executed will return a string
|
|
115
|
+
# that this method can then extract the real revision from.
|
|
116
|
+
def query_revision(revision)
|
|
117
|
+
raise NotImplementedError, "`query_revision' is not implemented by #{self.class.name}"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Returns the revision number immediately following revision, if at
|
|
121
|
+
# all possible. A block should always be passed to this method, which
|
|
122
|
+
# accepts a command to invoke and returns the result, although a
|
|
123
|
+
# particular SCM's implementation is not required to invoke the block.
|
|
124
|
+
#
|
|
125
|
+
# By default, this method simply returns the revision itself. If a
|
|
126
|
+
# particular SCM is able to determine a subsequent revision given a
|
|
127
|
+
# revision identifier, it should override this method.
|
|
128
|
+
def next_revision(revision)
|
|
129
|
+
revision
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Should analyze the given text and determine whether or not a
|
|
133
|
+
# response is expected, and if so, return the appropriate response.
|
|
134
|
+
# If no response is expected, return nil. The +state+ parameter is a
|
|
135
|
+
# hash that may be used to preserve state between calls. This method
|
|
136
|
+
# is used to define how Minestrone should respond to common prompts
|
|
137
|
+
# and messages from the SCM, like password prompts and such. By
|
|
138
|
+
# default, the output is simply displayed.
|
|
139
|
+
def handle_data(state, stream, text)
|
|
140
|
+
logger.info "[#{stream}] #{text}"
|
|
141
|
+
nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Returns the name of the command-line utility for this SCM. It first
|
|
145
|
+
# looks at the :scm_command variable, and if it does not exist, it
|
|
146
|
+
# then falls back to whatever was defined by +default_command+.
|
|
147
|
+
#
|
|
148
|
+
# If scm_command is set to :default, the default_command will be
|
|
149
|
+
# returned.
|
|
150
|
+
def command
|
|
151
|
+
command = variable(:scm_command)
|
|
152
|
+
command = nil if command == :default
|
|
153
|
+
command || default_command
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# A helper method that can be used to define SCM commands naturally.
|
|
157
|
+
# It returns a single string with all arguments joined by spaces,
|
|
158
|
+
# with the scm command prefixed onto it.
|
|
159
|
+
def scm(*args)
|
|
160
|
+
[command, *args].compact.join(" ")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
# A helper for accessing variable values, which takes into
|
|
166
|
+
# consideration the current mode ("normal" vs. "local").
|
|
167
|
+
def variable(name, default = nil)
|
|
168
|
+
if local? && configuration.exists?("local_#{name}".to_sym)
|
|
169
|
+
return configuration["local_#{name}".to_sym].nil? ? default : configuration["local_#{name}".to_sym]
|
|
170
|
+
else
|
|
171
|
+
configuration[name].nil? ? default : configuration[name]
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# A reference to a Logger instance that the SCM can use to log
|
|
176
|
+
# activity.
|
|
177
|
+
def logger
|
|
178
|
+
@logger ||= variable(:logger) || Minestrone::Logger.new(:output => STDOUT)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# A helper for accessing the default command name for this SCM. It
|
|
182
|
+
# simply delegates to the class' +default_command+ method.
|
|
183
|
+
def default_command
|
|
184
|
+
self.class.default_command
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# A convenience method for accessing the declared repository value.
|
|
188
|
+
def repository
|
|
189
|
+
variable(:repository)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def arguments(command = :all)
|
|
193
|
+
value = variable(:scm_arguments)
|
|
194
|
+
|
|
195
|
+
if value.is_a?(Hash)
|
|
196
|
+
value = value[command]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
value
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|