Imported Upstream version 3.2.0
Sebastien Badia
10 years ago
0 | *.gem | |
1 | *.rbc | |
2 | .bundle | |
3 | .config | |
4 | .yardoc | |
5 | Gemfile.lock | |
6 | InstalledFiles | |
7 | _yardoc | |
0 | 8 | coverage |
1 | doc | |
9 | doc/ | |
10 | lib/bundler/man | |
2 | 11 | pkg |
12 | rdoc | |
13 | spec/reports | |
14 | test/tmp | |
15 | test/version_tmp | |
16 | tmp | |
17 | .rspec-local | |
18 | .ruby-version | |
19 | _site | |
20 | .rspec | |
3 | 21 | *.swp |
4 | *.patch | |
5 | *.gem | |
6 | *.sh | |
7 | .DS_Store | |
8 | .bundle/* | |
9 | Gemfile.lock |
0 | 0 | language: ruby |
1 | 1 | rvm: |
2 | - 1.8.7 | |
2 | - 2.1.0 | |
3 | - 2.0.0 | |
4 | - 1.9.3 | |
3 | 5 | - 1.9.2 |
4 | - 1.9.3 | |
5 | - ree | |
6 | - rbx | |
7 | script: bundle exec rake spec | |
8 | cache: bundler | |
9 | bundler_args: --without cucumber |
0 | ## 2.12.0 / April 13 2012 | |
1 | ||
2 | This release revserts the very verbose logging introduced in the previous version, it also | |
3 | enables a handful of power-user features which are largely un-documented, but shouldn't be | |
4 | important unless you are looking for them. Undocumented code shouldn't scare you, simply | |
5 | read through deploy.rb in the Gem if you want to know how a new feature works! | |
6 | ||
7 | * Update mapped commands to remove symlink deprecation warning. Despo Pentara (despo) | |
8 | * Add the "rpm" remote dependency. Nick Hoffman (nickhoffman) | |
9 | * Add commented deploy:cleanup task to default recipe. Jean-Philippe Doyle (j15e) | |
10 | * Teach deploy:web:enable to fail gracefully. Lee Marlow (lmarlow) | |
11 | * Ticket 193 alias task show wrong name when it is not overridden. Rafa García (rgo) | |
12 | * Allow configuration of which roles assets are precompiled on. Frederick Cheung (fcheung) | |
13 | * Fix transfer action to honor dry-run flag. Serg Podtynnyi (shtirlic) | |
14 | * Changed single to double quotes for Windows, fixes a Windows bug in the HG module. Matthew J Morrison (mattjmorrison) | |
15 | * Add UnsharedRemoteCache (copied from eycap gem). Ben Symonds (bensymonds) | |
16 | ||
17 | As ever, a sincere thanks to all contributors, and do not hesitate to contact me if this | |
18 | release causes problems for you. | |
19 | ||
20 | ## 2.11.0 / Febuary 20 2012 | |
21 | ||
22 | This release replaces and fixes a broken 2.10.0 release (see below for | |
23 | information) | |
24 | ||
25 | This release includes all fixes as documented for 2.10.0, plus additional code | |
26 | cleanup (SHA: 3eecac2), as well as changing the public API from | |
27 | `deploy:symlink`, to `deploy:create_symlink` | |
28 | ||
29 | * Remove a testing dependency on `ruby-debug` (Lee Hambley, reported by Serg | |
30 | Podtynnyi) | |
31 | ||
32 | * Formerly deprecate `deploy:symlink`, this move was prompted by the Rake | |
33 | namespace fiasco of their 0.9.0 release, and is replaced by | |
34 | `deploy:create_symlink`. If you are looking for a place to hook asset related | |
35 | tasks, please consider `deploy:finalize_update`. Replaced by | |
36 | `deploy:create_symlink`, `deploy:symlink` now raises a deprecation warning, | |
37 | and defers to `deploy:symlink`. (Lee Hambley) | |
38 | ||
39 | * Update the 2.10.0 changelog. (Lee Hambley, reported by Jérémy Lecour) | |
40 | ||
41 | ## 2.10.0 / Febuary 19 2012 | |
42 | ||
43 | If you are reading this after Febuary 20th 2012, do not be surprised when you | |
44 | cannot find 2.10.0 on Rubygems.org, it has been removed because of a breaking | |
45 | API change. It is replaced logically enough by 2.11.0 where the API is still | |
46 | changed, but now issues a warning and falls back to the expected behaviour. | |
47 | ||
48 | The CHANGELOG for this release was updated retrospectively, I'm sorry I missed | |
49 | that when releasing the gem, 2.10.0 apparently not my finest hour as a | |
50 | maintainer. | |
51 | ||
52 | Ths fixes in this release include | |
53 | ||
54 | * Include sample NGinx config for `deploy:web:disable`(added by Roger Ertesvåg) | |
55 | ||
56 | * Fix gemspec time format warning (reported by Tony Arcieri, fixed by building the Gem against Ruby-1.9.3) | |
57 | ||
58 | * Finally removed deprecated `before_` and `after_` tasks. (Lee Hambley) | |
59 | ||
60 | * Rake 0.9.x compatibility (reported by James Miller, fixed by Lee Hambley) | |
61 | ||
62 | * More detailed logging output (fixed by Huang Liang) | |
63 | ||
64 | * Includes multistage, without `capistrano-ext`. `require 'capistrano/ext/multistage'` (fixed by Lee Hambley) | |
65 | ||
66 | ## 2.9.0 / September 24 2011 | |
67 | ||
68 | A vairly heavy release, including some new features which most of you won't | |
69 | need, but power users have contributed, this also marks the beginning of the | |
70 | end of the 2.x series, more information will follow in due course, but with | |
71 | the proliferation of Bundler, and better ways to do deployment, we will be | |
72 | introducing heavier changes to Capistrano to keep the tool current. | |
73 | ||
74 | **Please note, following some reported problems with the asset pipeline code | |
75 | being not found, remember Capistrano needs to be in your Gemfile, and as such | |
76 | needs to be run with Bundler, otherwise you risk loading a system-wide version | |
77 | of Capistrano who's behaviour might be different from that specified in your | |
78 | Gemfile. This is also good practice because much of the deploy logic resides | |
79 | in the Gem, and you wouldn't want that to change without your knowledge. Rails | |
80 | applications include Cap in the Gemfile anyway, you should follow this | |
81 | convention.** | |
82 | ||
83 | * find_servers() will no longer raise if there are no servers, this behaviour | |
84 | can be modified by way of the `:on_no_matching_servers` option. Thanks to | |
85 | `@ppgengler`. | |
86 | ||
87 | * Short Git SHA1 fragments are now supported in commands such as `cap deploy | |
88 | -s revision=d7e99f` thanks to `@ndbroadbent`. | |
89 | ||
90 | * One can now specify individual SCM commands by setting | |
91 | `:scm_arguments_<command_name>`. Thanks to `@alextk`. | |
92 | ||
93 | * Travis CI build now passes thanks to @andrew, build tested against MRI | |
94 | `1.8.7`. `1.9.2` and `REE`. | |
95 | ||
96 | * Support for Perforce labels, I don't know much about this, but I believe | |
97 | it's much like deploying a Git tag, thanks to `@ak47`. | |
98 | ||
99 | * Git SCM now correctly adheres to the `:scm_verbose` setting. Thanks | |
100 | `@dubek`. | |
101 | ||
102 | * `set()` can now be used to set a `false` value, previously this was a no-op. | |
103 | Thanks to `@nilbus`. | |
104 | ||
105 | * Support for Git 1.6x submodules, The Git SCM strategy now queries Git on the | |
106 | server-side to ensure it supports the `--recursive` flag, if it doesn't then | |
107 | it will fall back to using the long-hand. Many thanks to all those involved in | |
108 | the discussion surrounding this topic, and to `@nilbus` for a beautifully | |
109 | clean solution which doesn't hold us back. | |
110 | ||
111 | * When using `:cached_copy` with Subversion, use `svn switch` to for more | |
112 | reliable switching of branches/etc. Thanks to `@iGEL` for the patch that we | |
113 | accepted finally, and to `@richmeyers` who also submitted a patch and | |
114 | contributed to the discssion. | |
115 | ||
116 | Other cleanups and minor improvements to the code and tests were committed by yours truly | |
117 | (@leehambley), @maxim, @ak47 and @andrew). | |
118 | ||
119 | ## 2.8.0 / August 3 2011 | |
120 | ||
121 | A short release, after the last. Announcing Rails 3.1 asset pipeline support. | |
122 | ||
123 | The asset pipeline support requires an additiona `load` in your `Capfile`. | |
124 | ||
125 | You can see information pertaining to the pull request, including the inline | |
126 | comments here: https://github.com/capistrano/capistrano/pull/35 | |
127 | ||
128 | Documentation will be available soon in the wiki. | |
129 | ||
130 | * Drop-In Rails 3.1 asset pipeline support. (Chris Griego) | |
131 | ||
132 | ## 2.7.0 / August 3 2011 | |
133 | ||
134 | A fairly substantial release. There are fixes so that current_release works | |
135 | during dry-runs, (although, apparently still not with bundler.) | |
136 | ||
137 | The test-suite was also modified to work with Ruby 1.9.2, except in one case | |
138 | where Ruby 1.9.x calls `to_ary` and `to_a` on mocks, which still makes an | |
139 | error. 1.9.x has always been supported, but due to lack of maintenance on my | |
140 | part the tests didn't ever pass. | |
141 | ||
142 | The `start`, `stop` and `restart` tasks have been reduced to mere hooks into | |
143 | which extensions can define their own functionality. | |
144 | ||
145 | The `readme` was also slightly improved, simply tweaks to express how best to | |
146 | run the test suite. | |
147 | ||
148 | * Ensure dry-run works with `:current_release` variable (Carol Nichols) | |
149 | * Added a new variable `:git_submodules_recursive`, setting the value to false | |
150 | will ensure Git doesn't recursively initialize and checkout submodules. (Konstantin Kudryashov) | |
151 | * Added an additional task option, `:on_no_matching_servers`, setting the | |
152 | value to `:continue` will ensure tasks with no matched servers continue | |
153 | without error, instead of raising `Capistrano::NoMatchingServersError` as was | |
154 | the previous behaviour. (Chris Griego) | |
155 | ||
156 | A huge thanks to all contributors, as always! | |
157 | ||
158 | Remember: @capistranorb on twitter for news. | |
159 | ||
160 | ## 2.6.1 / June 25 2011 | |
161 | ||
162 | A short maintenance release, Some fixes to the verbose flag inside the Git SCM | |
163 | as well as another argument for the (internal) `variable()` command, offering | |
164 | a default. The Git SCM is now verbose by default, but can be disabled by | |
165 | setting `:scm_verbose` to false. | |
166 | ||
167 | There has been an additional method added to string, within the context of the | |
168 | test suite, I'm always sketchy about adding additional methods to core | |
169 | classes, but it's a short term fix until I make the time to patch the test | |
170 | suite not to compare strings literally. The method is `String#compact`, and is | |
171 | implemented simply as `self.gsub(/\s+/, ' ')`. | |
172 | ||
173 | Here's the run-down of changes, and their committers, as always - a huge thank | |
174 | you to the community that continues to drive Capistrano's development. | |
175 | ||
176 | * `deploy:setup` now respects `:group_writable` (Daniel Duvall) | |
177 | * Fixes to `:scm_verbose` for the Git module (defaults to On.) (Matthew Davies) | |
178 | * Will now copy hidden files in the project's root into the release | |
179 | directory (Mark Jaquith) | |
180 | * Now handles closing already-dead connections in a sane way (does not raise | |
181 | an exception) (Will Bryant) | |
182 | * Renamed `Capistrano::VERSION::TINY` to `Capistrano::VERSION::PATCH` (Lee | |
183 | Hambley) | |
184 | * Removed the `VERSION` file (Lee Hambley) | |
185 | ||
186 | ## 2.6.0 / May 3 2011 | |
187 | ||
188 | A rather large release, feature-version bump because of the new | |
189 | multiple-gateways feature as implemented by Ryan Duryea (way to go!) | |
190 | ||
191 | Please also note from this release that if you use Git submodules, the | |
192 | Git-version requirement for the new implementation is now >= 1.5.6, from | |
193 | previously un-documented. (1.5.6 is new-enough that I think this is | |
194 | acceptable) | |
195 | ||
196 | * Upgrade Net::SSH-gateway dependency to 1.1 (fixes a thread-deadlocking bug on MRI 1.9) | |
197 | * Respect "dry-run" on transfer methods (Florian Frank) | |
198 | * Add support for multiple gateways: (Ryan Duryea) | |
199 | set :gateway, { | |
200 | 'gate1.example.com' => 'server1.example.com', | |
201 | [ 'gate2.example.com', 'gate3.example.com' ] => [ 'server5.example.com', 'server6.example.com' ] | |
202 | } | |
203 | * Properly support nested Git submodules, moves Git requirement to >= 1.5.6 [if you rely upon submodules] (Ken Miller) | |
204 | * Fetch tags into the remote cache, allows deploying a tag when using Git, with the remote_cache strategy (Florian Frank) | |
205 | * Various fixes to path handling bugs in the copt strategy. (Philippe Rathé) | |
206 | ||
207 | ## 2.5.21 / April 6 2011 | |
208 | ||
209 | * Fixed to follow best-practice guidelines from Bundler (Ben Langfeld) | |
210 | * No longer force a gemset for Capistrano development. (Ben Langfeld) | |
211 | ||
212 | ## 2.5.20 / March 16 2011 | |
213 | ||
214 | * `deploy:migrations` will now always operate on the latest_release, not | |
215 | current_release (Mike Vincent) | |
216 | * Adds a check for the presence of `rsync` when using the copy strategy with `rsync`. (Chris Griego) | |
217 | * Do not try to look up the `:release_path` on servers which are defined `:no_release` (Chris Griego) | |
218 | * Tiny patch to the `CVS` SCM code to be Ruby 1.9 compatible (Martin Carpenter) | |
219 | * Changed the default `Git` submodule behaviour to use `--recursive`, Lighthouse Issue #176. (Lee Hambley) | |
220 | * `:public_children` can now be `set()`, the default is unchanged, thanks (Chris Griego) | |
221 | * Fixing the load path in the default `Capfile` to search vendored/unpacked Gems. Lighthouse Issue #174 (Mari Carmen/Rafael García) | |
222 | * Adds a `maintenance_basename` variable (default value is `maintenance`) to allow you to set the maintenance page name (Celestino Gomes) | |
223 | * Spelling fixes in inline-documentation (Tom Copeland) | |
224 | * Make `zip` and `tar` handle symlinks the same way (zip follows symlinks by default, tar needs the option `-h`) (Ross Cooperman) | |
225 | ||
226 | ## 2.5.19 / June 21, 2010 | |
227 | ||
228 | * Small bug fixes, no improvements for people who weren't experiencing problems anyway. | |
229 | ||
230 | ## 2.5.18 / March 14, 2010 | |
231 | ||
232 | Small fix for rolling back if a shell scripts exits non-zero; enabled a rollback if git (or other) externals fail during the deploy. | |
233 | ||
234 | * #151 check return code status of system command to create local copy and rollback if not 0 (David King) | |
235 | ||
236 | ## 2.5.17 / February 27, 2010 | |
237 | ||
238 | Various small bug fixes. | |
239 | ||
240 | ## 2.5.16 / February 14, 2010 | |
241 | ||
242 | Fixed a small regression in 2.5.15 | |
243 | ||
244 | ## 2.5.15 / 14 February 2010 | |
245 | ||
246 | Fixes a feature request not to overwrite roles when using the ROLES environmental variable. | |
247 | ||
248 | * #126 - The option to not overwriting the roles which are defined in the task definition. | |
249 | * Removed the `upgrade` file as it has been a couple of years since 1.x was in the wild. | |
250 | * Slight internal re-factor of the way we calculate the `version` | |
251 | ||
252 | ## 2.5.14 / 18 January 2010 | |
253 | ||
254 | Fixes a low-value bug, thanks to Chris G for the well submitted patch: | |
255 | ||
256 | * #139 - Improves consistency of variable lookup, scm variables with a local_ prefix will be honoured with priority locally (Chris Griego) | |
257 | ||
258 | ## 2.5.13 / 6 January 2010 | |
259 | ||
260 | * Small maintenance release: | |
261 | ||
262 | * #118 - Modified CLI test to not load user or system configuration file (Emily Price) | |
263 | * #88 - Re-fixed a problem here, massive apologies to all concerned. (Hangover from 2.5.12) | |
264 | ||
265 | ## 2.5.12 / 5 January 2010 | |
266 | ||
267 | * Tweak the directory version listing (caused a lot of problems, please upgrade immediately) | |
268 | ||
269 | ## 2.5.11 / December 2009 | |
270 | ||
271 | * Deprecations and other small changes | |
272 | ||
273 | ## 2.5.10 / 3 November 2009 | |
274 | ||
275 | * Fixes Darcs remote repository problem when using the copy strategy [Alex `regularfry` Young] | |
276 | * Documentation improvements for embedding Capistrano [Lee Hambley] | |
277 | * Fixes ticket #95 -formally deprecating the before_something and after_something methods [Lee Hambley] | |
278 | ||
279 | ## 2.5.9 / 1 August 2009 | |
280 | ||
281 | * Adds support for customizing which `tar` command to use. [Jeremy Wells] | |
282 | ||
283 | * Fixes a couple of documentation problems, typos and worse. [Lee Hambley] | |
284 | ||
285 | * #105 - Add skip_hostfilter option to find_servers() [Eric] | |
286 | * #103 - Using non-master branch fails with Ruby 1.9 [Suraj Kurapati] | |
287 | * #96 - Tweak for 1.9 Compatibility | |
288 | * #79 - Capistrano hangs on shell command for many computers | |
289 | * #77 - Copy command doesn't work on Solaris due to tar/gtar | |
290 | * #76 - Invalid Subversion URL | |
291 | * Improved web:disable task, now suggests a .htaccess block to use suggested by Rafael García | |
292 | * Includes more logger options (can now select stdout, stderr of a file) [Rafael García] | |
293 | ||
294 | ## 2.5.8 / July 2009 | |
295 | ||
296 | * Fixes a problem in 2.5.7 where deploy:finalize_update had been badly merged. | |
297 | ||
298 | ## 2.5.6 & 2.5.7 / July 2009 | |
299 | ||
300 | * 2.5.7 masks a broken 2.5.6 release that was accidentally mirrored via Rubyforge. | |
301 | ||
302 | * Clean the cached git repository [Graeme Mathieson] | |
303 | ||
304 | * Fixes perforce issues reported at http://bit.ly/wt0es [Scott Johnson] | |
305 | ||
306 | * Improved back-tick handling code in relation to the above. | |
307 | ||
308 | * Fixes a Git issue when submodules update upstream. (via mailing list) [sneakin] | |
309 | ||
310 | * Capify now creates the config directory in directories without one. | |
311 | ||
312 | ## 2.5.5 / 24 Feb 2009 | |
313 | ||
314 | * Make sure role(:foo) actually declares an (empty) role for :foo, even without server arguments [Jamis Buck] | |
315 | ||
316 | ||
317 | ## 2.5.4 / 4 Feb 2009 | |
318 | ||
319 | * When using rsync with the remote_cache strategy include -t switch to preserve file times [Kevin McCarthy] | |
320 | ||
321 | * Bump Net::SSH dependency to version 2.0.10 [Jamis Buck] | |
322 | ||
323 | * Use 'user' from .ssh/config appropriately [Jamis Buck] | |
324 | ||
325 | * Allow respond_to?() method to accept optional second parameter (include_priv) [Matthias Marschall] | |
326 | ||
327 | * Make sure sudo prompts are retried correctly even if "try again" and the prompt appear in the same text chunk from the server [Jamis Buck] | |
328 | ||
329 | * Add supported environment variables to -H output [François Beausoleil] | |
330 | ||
331 | ||
332 | ## 2.5.3 / December 6, 2008 | |
333 | ||
334 | * Make previous_release return nil if there is no previous release [Mathias Meyer] | |
335 | ||
336 | * Play nice with rubies that don't inspect terminals well (ie. JRuby) by defaulting screen columns to 80 [Bob McWhirter] | |
337 | ||
338 | * Rollback of deploy:symlink would explode if there was no previous revision to rollback to [Jamis Buck] | |
339 | ||
340 | * Fix bug in transfer.rb that caused get/put/upload/download to ignore blocks passed to them [arika] | |
341 | ||
342 | * Fix issue with git SCM that caused "Unable to resolve revision" errors when there was trailing whitespace in git's output [Mark Zuneska, Daniel Berlinger and Evan Closson] | |
343 | ||
344 | ||
345 | ## 2.5.2 / November 13, 2008 | |
346 | ||
347 | * Fix issue with git SCM that caused "Unable to resolve revision for 'HEAD'" errors on deploy [Jamis Buck] | |
348 | ||
349 | ||
350 | ## 2.5.1 / November 7, 2008 | |
351 | ||
352 | * Add -t (--tools) switch for better task lists for external tools [Jamis Buck] | |
353 | ||
354 | * Make the RemoteDependency#try method use invoke_command instead of run, for sudo-ability [Matthias Marschall] | |
355 | ||
356 | * Make locally executed commands in Windows more Windows-friendly [esad@esse.at] | |
357 | ||
358 | * Added :scm_arguments variable for custom SCM arguments (subversion-only, currently) [David Abdemoulaie] | |
359 | ||
360 | * Don't emit -p for sudo when :sudo_prompt is blank [Matthias Marschall] | |
361 | ||
362 | * Copy symlinks when using rsync [Paul Paradise] | |
363 | ||
364 | * Make sure git query-revision matches on exact branch name [grant@nightriot.com] | |
365 | ||
366 | * Use -T <arg> to filter listed tasks by a pattern [Mathias Meyer, Geoffrey Grosenbach] | |
367 | ||
368 | * Expose the #scm method on SCM::Base for building custom scm commands [Mathias Meyer] | |
369 | ||
370 | * Start logging some locally executed commands [springyweb] | |
371 | ||
372 | * Added HOSTFILTER environment variable for constraining tasks so they run only on hosts matching the given list of servers [Walter Smith] | |
373 | ||
374 | * Make sure the glob matching for copy excludes does not delete parent directories [Fabio Akita] | |
375 | ||
376 | * Ruby 1.9 compatibility [Jamis Buck] | |
377 | ||
378 | ||
379 | ## 2.5.0 / August 28, 2008 | |
380 | ||
381 | * Allow :gateway to be set to an array, in which case a chain of tunnels is created [Kerry Buckley] | |
382 | ||
383 | * Allow HOSTS spec to override even non-existent roles [Mike Bailey] | |
384 | ||
385 | * Sort releases via "ls -xt" instead of "ls -x" to allow for custom release names [Yan Pritzker] | |
386 | ||
387 | * Convert arguments to -s and -S into integers, booleans, etc. based on whether the arguments appear to be those types [Jamis Buck] | |
388 | ||
389 | * Add descriptions of -n and -d to the verbose help text [Jamis Buck] | |
390 | ||
391 | * Make rollbacks work with processes that need the current directory to be valid in order to restart properly (e.g. mongrel_rails) [Jamis Buck] | |
392 | ||
393 | * Rename deploy:rollback_code to deploy:rollback:code [Jamis Buck] | |
394 | ||
395 | * Added parallel() helper for executing multiple different commands in parallel [Jamis Buck] | |
396 | ||
397 | * Make sure a task only uses the last on_rollback block, once, on rollback [Jamis Buck] | |
398 | ||
399 | * Add :shared_children variable to customize which subdirectories are created by deploy:setup [Jonathan Share] | |
400 | ||
401 | * Allow filename globbing in copy_exclude setting for the copy strategy [Jonathan Share] | |
402 | ||
403 | * Allow remote_cache strategy to use copy_exclude settings (requires rsync) [Lewis Mackenzie] | |
404 | ||
405 | * Make None SCM module work in Windows [Carlos Kozuszko] | |
406 | ||
407 | * Recognize mingw as a Windows platform [Carlos Kozuszko] | |
408 | ||
409 | * Fixed failing tests in Windows [Carlos Kozuszko] | |
410 | ||
411 | * Made :scm_auth_cache control whether password option is emitted in subversion module [Brendan Schwartz] | |
412 | ||
413 | * Fixed timestamp bug in CVS module [Jørgen Fjeld] | |
414 | ||
415 | * Added -n/--dry-run switch, to display but not execute remote tasks [Paul Gross] | |
416 | ||
417 | ||
418 | ## 2.4.3 / June 28, 2008 | |
419 | ||
420 | * Fix gem dependencies so gem actually understands them [Jamis Buck] | |
421 | ||
422 | ||
423 | ## 2.4.2 / June 27, 2008 | |
424 | ||
425 | * Specify gem dependencies in rakefile [Jamis Buck] | |
426 | ||
427 | ||
428 | ## 2.4.1 / June 27, 2008 | |
429 | ||
430 | * Use Echoe to manage the Rakefile [Jamis Buck] | |
431 | ||
432 | * Let Net::SSH manage the default SSH port selection [Ben Lavender] | |
433 | ||
434 | * Changed capture() helper to not raise an exception on error, but to warn instead [Jeff Forcier] | |
435 | ||
436 | ||
437 | ## 2.4.0 / June 13, 2008 | |
438 | ||
439 | * Added :normalize_asset_timestamps option to deployment, defaulting to true, which allows asset timestamping to be disabled [John Trupiano] | |
440 | ||
441 | ||
442 | ## 2.4.0 Preview Release #1 (2.3.101) / June 5, 2008 | |
443 | ||
444 | * Only make deploy:start, deploy:stop, and deploy:restart try sudo as :runner. The other sudo-enabled tasks (deploy:setup, deploy:cleanup, etc.) will now use the :admin_runner user (which by default is unset). [Jamis Buck] | |
445 | ||
446 | * Make sure triggers defined as a block inherit the scope of the task they are attached to, instead of the task they were called from [Jamis Buck] | |
447 | ||
448 | * Make deploy:upload use the upload() helper for more efficient directory processing [Jamis Buck] | |
449 | ||
450 | * Make deploy:upload accept globs [Mark Imbriaco] | |
451 | ||
452 | * Make sure the host is reported with the output from scm_run [Jamis Buck] | |
453 | ||
454 | * Make git SCM honor the :scm_verbose option [Jamis Buck] | |
455 | ||
456 | * Don't follow symlinks when using :copy_cache [Jamis Buck] | |
457 | ||
458 | * If :mode is given to upload() helper, do a chmod after to set the mode [Jamis Buck] | |
459 | ||
460 | * Fix load_from_file method for windows users [Neil Wilson] | |
461 | ||
462 | * Display a deprecation error if a remote git branch is specified [Tim Harper] | |
463 | ||
464 | * Fix deployment recipes to use the updated sudo helper [Jamis Buck] | |
465 | ||
466 | * Enhance the sudo helper so it can be used to return the command, instead of executing it [Jamis Buck] | |
467 | ||
468 | * Revert "make sudo helper play nicely with complex command chains", since it broke stuff [Jamis Buck] | |
469 | ||
470 | * Make set(:default_shell, false) work for not using a shell on a per-command basis [Ryan McGeary] | |
471 | ||
472 | * Improved test coverage [Ryan McGeary] | |
473 | ||
474 | * Fixed "coverage" take task [Ryan McGeary] | |
475 | ||
476 | * Use upload() instead of put() with the copy strategy [Jamis Buck] | |
477 | ||
478 | * Revert the "git fetch --tags" change, since it didn't work as expected [Jamis Buck] | |
479 | ||
480 | * Fix deploy:pending when using git SCM [Ryan McGeary] | |
481 | ||
482 | * Make sure deploy:check works with :none scm (which has no default command) [Jamis Buck] | |
483 | ||
484 | * Add debug switch for enabling conditional execution of commands [Mark Imbriaco] | |
485 | ||
486 | ||
487 | ## 2.3.0 / May 2, 2008 | |
488 | ||
489 | * Make deploy:setup obey the :use_sudo and :runner directives, and generalize the :use_sudo and :runner options into a try_sudo() helper method [Jamis Buck] | |
490 | ||
491 | * Make sudo helper play nicely with complex command chains [Jamis Buck] | |
492 | ||
493 | * Expand file-transfer options with new upload() and download() helpers. [Jamis Buck] | |
494 | ||
495 | * Allow SCP transfers in addition to SFTP. [Jamis Buck] | |
496 | ||
497 | * Use Net::SSH v2 and Net::SSH::Gateway. [Jamis Buck] | |
498 | ||
499 | * Added #export method for git SCM [Phillip Goldenburg] | |
500 | ||
501 | * For query_revision, git SCM used git-rev-parse on the repo hosting the Capfile, which may NOT be the same tree as the actual source reposistory. Use git-ls-remote instead to resolve the revision for checkout. [Robin H. Johnson] | |
502 | ||
503 | * Allow :ssh_options hash to be specified per server [Jesse Newland] | |
504 | ||
505 | * Added support for depend :remote, :file to test for existence of a specific file [Andrew Carter] | |
506 | ||
507 | * Ensure that the default run options are mixed into the command options when executing a command from the cap shell [Ken Collins] | |
508 | ||
509 | * Added :none SCM module for deploying a specific directory's contents [Jamis Buck] | |
510 | ||
511 | * Improved "copy" strategy supports local caching and pattern exclusion (via :copy_cache and :copy_exclude variables) [Jamis Buck] | |
512 | ||
513 | ||
514 | ## 2.2.0 / February 27, 2008 | |
515 | ||
516 | * Fix git submodule support to init on sync [halorgium] | |
517 | ||
518 | * Add alternative server-centric role definition method [James Duncan Davidson] | |
519 | ||
520 | * Add support for password prompts from the Mercurial SCM [ches] | |
521 | ||
522 | * Add support for :max_hosts option in task definition or run() [Rob Holland <rob@inversepath.com>] | |
523 | ||
524 | * Distributed git support for better operability with remote_cache strategy [voidlock] | |
525 | ||
526 | * Use a default line length in help text if line length is otherwise too small [Jamis Buck] | |
527 | ||
528 | * Fix incorrect reference to the 'setup' task in task documentation [rajeshduggal] | |
529 | ||
530 | * Don't try to kill the spawner process on deploy:stop if no spawner process exists [Jamis Buck] | |
531 | ||
532 | * Dynamic roles (e.g. role(:app) { "host.name" }) [dmasover] | |
533 | ||
534 | * Implement Bzr#next_revision so that pending changes can be reported correctly [casret] | |
535 | ||
536 | * Use a proper export command for bzr SCM [drudru] | |
537 | ||
538 | * Use checkout instead of merge for git SCM [nuttycom] | |
539 | ||
540 | * Fix typo in Subversion SCM module, encountered when an update fails [kemiller] | |
541 | ||
542 | * Fix documentation typo in upload.rb [evolving_jerk] | |
543 | ||
544 | * Added test case to show that the :scm_command is honored by the git SCM module [grempe] | |
545 | ||
546 | * Fail gracefully when double-colons are used to delimit namespaces [richie] | |
547 | ||
548 | * Add support for :git_enable_submodules variable, to enable submodules with the git SCM [halorgium] | |
549 | ||
550 | * If subversion asks for a password, prompt as a last resort [Jamis Buck] | |
551 | ||
552 | * Use checkout --lightweight for bzr checkout, instead of branch [michiels] | |
553 | ||
554 | * Make sure bzr SCM works when revision is head (or unspecified) [michiels] | |
555 | ||
556 | * Support p4sync_flags and p4client_root variables for Perforce [gseidman] | |
557 | ||
558 | * Prepare for Net::SSH v2 by making sure Capistrano only tries to load Net::SSH versions less than 1.99.0 [Jamis Buck] | |
559 | ||
560 | ||
561 | ## 2.1.0 / October 14, 2007 | |
562 | ||
563 | * Default to 0664 instead of 0660 on upload [Jamis Buck] | |
564 | ||
565 | * Fix deploy:pending to query SCM for the subsequent revision so that it does not include the last deployed change [Jamis Buck] | |
566 | ||
567 | * Prefer 'Last Changed Rev' over 'Revision' when querying latest revision via Subversion [Jamis Buck] | |
568 | ||
569 | * Explicitly require 'stringio' in copy_test [mislav] | |
570 | ||
571 | * When Subversion#query_revision fails, give a more sane error [Jamis Buck] | |
572 | ||
573 | * Don't run the upgrade:revisions task on non-release servers [Jamis Buck] | |
574 | ||
575 | * Fix cap shell to properly recognize sudo prompt [Mark Imbriaco, barnaby, Jamis Buck] | |
576 | ||
577 | * Git SCM module [Garry Dolley, Geoffrey Grosenbach, Scott Chacon] | |
578 | ||
579 | * Use the --password switch for subversion by default, but add :scm_prefer_prompt variable (defaults to false) [Jamis Buck] | |
580 | ||
581 | ||
582 | ## 2.0.100 (2.1 Preview 1) / September 1, 2007 | |
583 | ||
584 | * capify-generated Capfile will autoload all recipes from vendor/plugins/*/recipes/*.rb [Graeme Mathieson] | |
585 | ||
586 | * Use sudo -p switch to set sudo password prompt to something predictable [Mike Bailey] | |
587 | ||
588 | * Allow independent configurations to require the same recipe file [Jamis Buck] | |
589 | ||
590 | * Set :shell to false to run a command without wrapping it in "sh -c" [Jamis Buck] | |
591 | ||
592 | * Don't request a pty by default [Jamis Buck] | |
593 | ||
594 | * Add a "match" remote dependency method [Adam Greene] | |
595 | ||
596 | * Allow auth-caching of subversion credentials to be enabled via :scm_auth_cache [tsmith] | |
597 | ||
598 | * Don't let a task trigger itself when used as the source for an "on" hook [Jamis Buck] | |
599 | ||
600 | * Avoid using the --password switch with subversion for security purposes [sentinel] | |
601 | ||
602 | * Add version_dir, current_dir, and shared_dir variables for naming the directories used in deployment [drinkingbird] | |
603 | ||
604 | * Use Windows-safe binary reads for reading file contents [Ladislav Martincik] | |
605 | ||
606 | * Add Accurev SCM support [Doug Barth] | |
607 | ||
608 | * Use the :runner variable to determine who to sudo as for deploy:restart [Graham Ashton] | |
609 | ||
610 | * Add Namespaces#top to always return a reference to the topmost namespace [Jamis Buck] | |
611 | ||
612 | * Change the "-h" output so that it does not say that "-q" is the default [Jamis Buck] | |
613 | ||
614 | ||
615 | ## 2.0.0 / July 21, 2007 | |
616 | ||
617 | * Make the "no matching servers" error more sane [halorgium] | |
618 | ||
619 | * Make sure the invoke task gives a sane error when the COMMAND value is omitted [halorgium] | |
620 | ||
621 | * Make sure variables are conditionally set in the deploy recipes, so as not to clobber values set elsewhere [Jamis Buck] | |
622 | ||
623 | * Fix "input stream is empty" errors from HighLine on prompt [Jamis Buck] | |
624 | ||
625 | * Added "synchronous_connect" setting to try and work around SFTP hangs for certain users [Jamis Buck] | |
626 | ||
627 | * Auto-require the SSH shell service, to avoid race conditions [Jamis Buck] | |
628 | ||
629 | * Add a millisecond sleep in upload to reduce CPU impact [Jamis Buck] | |
630 | ||
631 | * Allow the logger to be set via Configuration#logger= [Jamis Buck] | |
632 | ||
633 | * Allow $CAPISTRANO:HOST$ to be used in filenames to the put command [Jamis Buck] | |
634 | ||
635 | * Allow execute_on_servers to be called without a current task again [Jamis Buck] | |
636 | ||
637 | * Put $stdout in sync mode, so that Net::SSH prompts are displayed [Jamis Buck] | |
638 | ||
639 | * Make sure deploy:check aborts if it fails [Jamis Buck] | |
640 | ||
641 | * Spelling corrections in docs [Tim Carey-Smith, Giles Bowkett] | |
642 | ||
643 | ||
644 | ## 1.99.3 (2.0 Preview 4) / June 28, 2007 | |
645 | ||
646 | * Don't break task descriptions on a period that appears in the middle of a sentence [Jamis Buck] | |
647 | ||
648 | * Added support for :on_error => :continue in task definitions, allowing tasks to effectively ignore connection and execution errors that occur as they run [Rob Holland] | |
649 | ||
650 | * Use correct parameters for Logger constructor in the SCM and Strategy base initializers [Jamis Buck] | |
651 | ||
652 | * Set LC_ALL=C before querying the revision, to make sure the output is in a predictable locale and can be parsed predictably [via Leandro Nunes dos Santos] | |
653 | ||
654 | * Add :copy_remote_dir variable for the :copy strategy, to indicate where the archive should be copied to on the remote servers [Jamis Buck] | |
655 | ||
656 | * Make the awk use in the dependencies code work with POSIX awk [mcornick] | |
657 | ||
658 | * Make variable accesses thread safe [via Adrian Danieli] | |
659 | ||
660 | * Make user input for yes/no prompts work correctly in the Mercurial module [Matthew Elder] | |
661 | ||
662 | * Use single quotes to escape semicolon in find command, instead of a backslash [via michael.italia@gmail.com] | |
663 | ||
664 | * Better quoting of reserved characters in commands [Jamis Buck] | |
665 | ||
666 | * Make sure Net::SSH versions prior to 1.1.0 still work [Jamis Buck] | |
667 | ||
668 | * Allow the :hosts and :roles keys to accept lambdas, which will be evaluated lazily to allow runtime selection of hosts and roles in tasks [Jamis Buck] | |
669 | ||
670 | * Use `which' to test whether a command exists in the remote path, instead of `test -p' [Jamis Buck] | |
671 | ||
672 | * Make sure the connection factory is established synchronously, to avoid multiple gateway instances being spawned [Jamis Buck] | |
673 | ||
674 | * Make sure symlink and finalize_update tasks reference the most recent release when called by themselves [Jamis Buck] | |
675 | ||
676 | ||
677 | ## 1.99.2 (2.0 Preview 3) / June 15, 2007 | |
678 | ||
679 | * CVS SCM module [Brian Phillips] | |
680 | ||
681 | * Fix typo in Perforce SCM module [Chris Bailey] | |
682 | ||
683 | * ssh_options < server options when connecting [Jamis Buck] | |
684 | ||
685 | * Logger defaults to $stderr instead of STDERR [lhartley] | |
686 | ||
687 | * Use cp -RPp instead of -a in the remote cache strategy | |
688 | ||
689 | * Make the UploadError exception include an array of the hosts that failed [rob@inversepath.com] | |
690 | ||
691 | * Allow "empty" roles to be declared [Jamis Buck] | |
692 | ||
693 | * Mercurial SCM module [Tobias Luetke, Matthew Elder] | |
694 | ||
695 | * Invoke all commands via sh (customizable via :default_shell) [Jamis Buck] | |
696 | ||
697 | * Make sure all directories exist on each deploy which are necessary for subsequent commands to succeed, since some SCM's won't save empty directories [Matthew Elder] | |
698 | ||
699 | * Add :default_environment variable, which is applied to every command | |
700 | ||
701 | ||
702 | ## 1.99.1 (2.0 Preview 2) / May 10, 2007 | |
703 | ||
704 | * Fix some documentation typos [eventualbuddha] | |
705 | ||
706 | * Don't retry failed connections if an explicit auth_methods list is given [Chris Farms] | |
707 | ||
708 | * Added support for load and exit callbacks, which get invoked when all recipes have been loaded and when all requested tasks have been executed [Jamis Buck] | |
709 | ||
710 | * Added support for start and finish callbacks, which get invoked when any task is called via the command-line [Jamis Buck] | |
711 | ||
712 | * Make `capify' understand simple command-line switches [Jamis Buck] | |
713 | ||
714 | * Make the server definition itself available to SSH channels, rather than just the host name [Jamis Buck] | |
715 | ||
716 | * Identify servers by their complete credentials in logs, rather than simply by hostname [Jamis Buck] | |
717 | ||
718 | * Uniquely identify servers based on hostname, port, and username, instead of merely on hostname [Jamis Buck] | |
719 | ||
720 | * Allow (e.g.) scm_command and local_scm_command to be set in the event of different paths to the scm command on local vs. remote hosts. [Jamis Buck] | |
721 | ||
722 | * Kill the "deploy:app" namespace and move those tasks into deploy, directly. [Jamis Buck] | |
723 | ||
724 | * Make sure 'desc' applies to the next defined task, in any namespace. [Jamis Buck] | |
725 | ||
726 | * Fix shell so that servers for a task are correctly discovered. [Jamis Buck] | |
727 | ||
728 | * Added before(), after(), and on() callback creation methods. [Jamis Buck] | |
729 | ||
730 | * Fix broken check! method for some deployment strategies. [Jamis Buck] | |
731 | ||
732 | * deploy:cold should also run migrations before starting the app [Jamis Buck] | |
733 | ||
734 | * Make the copy strategy check out to a temporary directory [Jamis Buck] | |
735 | ||
736 | ||
737 | ## 1.99.0 (2.0 Preview 1) / April 24, 2007 | |
738 | ||
739 | * Add `capify' script to make it easier to prepare a project for deployment using cap [Jamis Buck] | |
740 | ||
741 | * Make sure the sudo helper understands the SuSE dialect of the sudo password prompt [Steven Wisener] | |
742 | ||
743 | * Fix synchronization issue with Gateway initialization [Doug Barth] | |
744 | ||
745 | * Added opt-in "compat" and "upgrade" recipes for tasks to aid in the upgrade process to Capistrano 2 [Jamis Buck] | |
746 | ||
747 | * The deployment recipes are now opt-in. Just do 'load "deploy"' in your recipe script. [Jamis Buck] | |
748 | ||
749 | * Added $CAPISTRANO:HOST$ placeholder in commands, which will be replaced with the name of the host on which the command is executing [Jamis Buck] | |
750 | ||
751 | * Added -e switch to explain specific task. Added -X to extend -x. Made -h much briefer. Added -T to list known tasks. [Jamis Buck] | |
752 | ||
753 | * Added namespaces for tasks [Jamis Buck] | |
754 | ||
755 | * Merged the Configuration and Actor classes, performed various other massive refactorings of the code [Jamis Buck] | |
756 | ||
757 | ||
758 | ## 1.4.1 / February 24, 2007 | |
759 | ||
760 | * Use the no-auth-cache option with subversion so that username/password tokens do not get cached by capistrano usage [jonathan] | |
761 | ||
762 | * Deprecated upper-cased variables [Jamis Buck] | |
763 | ||
764 | * Make sure Actor#get does not close the SFTP channel (so subsequent SFTP operations work) [Dov Murik] | |
765 | ||
766 | * Add :env option to 'run' (and friends) so that you can specify environment variables to be injected into the new process' environment [Mathieu Lajugie] | |
767 | ||
768 | ||
769 | ## 1.4.0 / February 3, 2007 | |
770 | ||
771 | * Use the auth info for subversion more consistently [Jamis Buck] | |
772 | ||
773 | * Add a "capture" helper, for capturing the stdout of a remote command and returning it as a string [Jamis Buck] | |
774 | ||
775 | * Add a "get" helper, to pull a file from a remote server to the localhost [bmihelac] | |
776 | ||
777 | * Fix gateway to actually increment local_port if a port is in use, so that multiple capistrano instances can run at the same time [Mark Imbriaco] | |
778 | ||
779 | * Refactor the permissions tweaking in update_code to a separate task so that people on shared hosts can override it as necessary [jaw6] | |
780 | ||
781 | * Set umask during the setup task, so that intermediate directories are created with the proper permissions [NeilW] | |
782 | ||
783 | * Removed -c/--caprc switch, since the new load order renders it meaningless (just use -f now) [Mike Bailey] | |
784 | ||
785 | * Make sure the standard recipe loads first, so that .caprc and friends can override what it defines. [Mike Bailey] | |
786 | ||
787 | * Add support for a system-wide capistrano config file [Mike Bailey] | |
788 | ||
789 | * Make cold_deploy call update instead of deploy (to avoid invoking the restart task). | |
790 | ||
791 | * Make the touch command run by update_code set the TZ to UTC, for consistent setting of asset timestamps. [NeilW] | |
792 | ||
793 | * Fix off-by-one bug in show_tasks width-computation [NeilW] | |
794 | ||
795 | ||
796 | ## 1.3.1 / January 5, 2007 | |
797 | ||
798 | * Fix connection problems when using gateways [Ezra Zygmuntowicz] | |
799 | ||
800 | ||
801 | ## 1.3.0 / December 23, 2006 | |
802 | ||
803 | * Deprecate rake integration in favor of invoking `cap' directly [Jamis Buck] | |
804 | ||
805 | * Make sure the CVS module references the repository explicitly in cvs_log [weyus@att.net] | |
806 | ||
807 | * Remove trace messages when loading a file [Jamis Buck] | |
808 | ||
809 | * Cleaner error messages for authentication failures and command errors [Jamis Buck] | |
810 | ||
811 | * Added support for ~/.caprc, also -x and -c switches. [Jamis Buck] | |
812 | ||
813 | * Updated migrate action to use db:migrate task in Rails instead of the deprecated migrate task [DHH] | |
814 | ||
815 | * Allow SSH user and port to be encoded in the hostname strings [Ezra Zygmuntowicz] | |
816 | ||
817 | * Fixed that new checkouts were not group-writable [DHH, Jamis Buck] | |
818 | ||
819 | * Fixed that cap setup would use 755 on the deploy_to and shared directory roots instead of 775 [DHH] | |
820 | ||
821 | * Don't run the cleanup task on servers marked no_release [Jamis Buck] | |
822 | ||
823 | * Fix typo in default_io_proc so it correctly checks the stream parameter to see if it is the error stream [Stephen Haberman] | |
824 | ||
825 | * Make sure assets in images, javascripts, and stylesheets are touched after updating the code, to ensure the asset timestamping feature of rails works correctly [Jamis Buck] | |
826 | ||
827 | * Added warning if password is prompted for and termios is not installed [John Labovitz] | |
828 | ||
829 | * Added :as option to sudo, so you can specify who the command is executed as [Mark Imbriaco] | |
830 | ||
831 | ||
832 | ## 1.2.0 / September 14, 2006 | |
833 | ||
834 | * Add experimental 'shell' task [Jamis Buck] | |
835 | ||
836 | * Display file for external configurations, rather than inspected proc. [Jamis Buck] | |
837 | ||
838 | * Connect to multiple servers in parallel, rather than serially. [Jamis Buck] | |
839 | ||
840 | * Add SCM module for Mercurial (closes #4150) [Matthew Elder] | |
841 | ||
842 | * Remove unused line in SCM::Base (closes #5619) [chris@seagul.co.uk] | |
843 | ||
844 | * More efficient "svn log" usage (closes #5620) [Anatol Pomozov] | |
845 | ||
846 | * Better support for key passphrases in the SVN module (closes #5920) [llasram@gmail.com] | |
847 | ||
848 | * Fix missing default for :local in cvs.rb (closes #3645) [jeremy@hinegardner.org] | |
849 | ||
850 | * Fix awkward spacing in gemspec file (closes #3888) [grant@antiflux.org] | |
851 | ||
852 | * Add support for :sudo variable to specify path to sudo (closes #4578) [timothee.peignier@tryphon.org] | |
853 | ||
854 | * Make previous_release return nil if there are no previous releases (closes #4959) [bdabney@cavoksolutions.com] | |
855 | ||
856 | * Uncache releases list after update_code is called so that newly released dir is included (closes #3766) [Jamis Buck] | |
857 | ||
858 | * Allow the subversion scm to accept HTTPS certificates (closes #4792) [Jamis Buck] | |
859 | ||
860 | * Make sure rollbacks occur within the scope of the task that triggered them [Jamis Buck] | |
861 | ||
862 | * Fixed the default recipe to work with setups that haven't yet gone pids [DHH] | |
863 | ||
864 | * Symlink and setup for shared/pids to tmp/pids [DHH] | |
865 | ||
866 | * Fix some incorrect usage text (closes #4507) [gerry_shaw@yahoo.com] | |
867 | ||
868 | * Added Actor#stream method that makes it easy to create cross-server streams [DHH]. Example: | |
869 | ||
870 | desc "Run a tail on multiple log files at the same time" | |
871 | task :tail_fcgi, :roles => :app do | |
872 | stream "tail -f #{shared_path}/log/fastcgi.crash.log" | |
873 | end | |
874 | ||
875 | * Make update_code and symlink a macro task under the name "update" for easy of deploy to servers that does not run fcgis [DHH] | |
876 | ||
877 | * Changed setup, update_code, rollback_code, and symlink to work on all servers instead of only those in the :app, :web, and :db roles. A server can opt out of being part of the release deployment by setting :no_release => true [DHH] | |
878 | ||
879 | * Added support for :except on task declarations as the opposite of :only [DHH]. Example: | |
880 | ||
881 | role :app, "192.168.0.2" | |
882 | role :file, "192.168.0.3", :no_release => true | |
883 | ||
884 | task :symlink, :except => { :no_release => true } do | |
885 | on_rollback { run "ln -nfs #{previous_release} #{current_path}" } | |
886 | run "ln -nfs #{current_release} #{current_path}" | |
887 | end | |
888 | ||
889 | cap symlink # will not run on 192.168.0.3 | |
890 | ||
891 | * Deprecate the -r/--recipe switch in favor of -f/--file (for more make/rake-like semantics) [Jamis Buck] | |
892 | ||
893 | * Fix gemspec to include a dependency on rake 0.7 [Jamis Buck] | |
894 | ||
895 | * Added respect for ENV["HOSTS"] that'll be used instead of the roles specified in the task definition [DHH]. Example: | |
896 | ||
897 | HOSTS=192.168.0.1 cap setup # one-off setup for that server, doesn't need to be prespecified in the recipes file | |
898 | ||
899 | * Added respect for ENV["ROLES"] that'll be used instead of the roles specified in the task definition [DHH]. Example: | |
900 | ||
901 | task :setup, :roles => [ :app, :web, :db ] | |
902 | # normally this would run every where | |
903 | end | |
904 | ||
905 | ROLES=app cap setup # this will only run for the app role, overwritting the default declaration | |
906 | ||
907 | * Added :hosts option to task definition that allows you to specify cross-cutting tasks [DHH]. Example: | |
908 | ||
909 | task :setup, :hosts => [ "06.example.com", "01.example.com" ] do | |
910 | # this task will happen on 06 and 01 regardless of which roles they belong to | |
911 | end | |
912 | ||
913 | * Fix operator precedence problem in script for touching the revisions.log #3223 [jason.garber@emu.edu] | |
914 | ||
915 | ||
916 | ## 1.1.0 / March 6th, 2006 | |
917 | ||
918 | * Simplify the generated capistrano.rake file, and make it easier to customize | |
919 | ||
920 | * Use rake-like command-line semantics ("cap deploy", in addition to "cap -a deploy") | |
921 | ||
922 | * Rename to capistrano | |
923 | ||
924 | * Make the generated capistrano.rake file use rake namespaces, and include all default tasks | |
925 | ||
926 | * Look for config/deploy.rb, capfile, and Capfile by default | |
927 | ||
928 | ||
929 | ## 1.0.1 / February 20th, 2006 | |
930 | ||
931 | * Fix broken switchtower_invoke function in switchtower.rake (missing require statement) | |
932 | ||
933 | ||
934 | ## 1.0.0 / Feburary 18th, 2006 | |
935 | ||
936 | * Make CVS module's :local value default to "." | |
937 | ||
938 | * Add "invoke" task for executing one-off commands | |
939 | ||
940 | * Make port selection smarter for gateway connections | |
941 | ||
942 | * Add extension mechanism for custom ST operations and third-party task libraries | |
943 | ||
944 | * Make ST rails rake tasks more configurable | |
945 | ||
946 | * Add Actor#current_task and simplify Task#servers | |
947 | ||
948 | * Add Actor#connect! method for working around lazy connection establishing | |
949 | ||
950 | * Make sure IO::TRUNC is specified for Net::SFTP uploads (#3510) | |
951 | ||
952 | * Add branch support to CVS [jeremy@hinegardner.org] (#3596) | |
953 | ||
954 | * Add bazaar-ng SCM module [Damien Merenne] | |
955 | ||
956 | * Add optional :svn_username and :svn_password variables | |
957 | ||
958 | * Allow Proc-valued variables to be set more conveniently (set(:foo) { "bar" }) | |
959 | ||
960 | * Add perforce SCM module [Richard McMahon] | |
961 | ||
962 | * Add bazaar (v1) SCM module [Edd Dumbill] (#3533) | |
963 | ||
964 | * Fix stftime format string used in CVS module to be Windows-compatible (fixes #3383) | |
965 | ||
966 | * Add an better error when a task is run and no servers match the required conditions | |
967 | ||
968 | * Add default spinner and cold_deploy tasks, and spinner_user variable | |
969 | ||
970 | * Changed restart_via variable to (boolean) use_sudo | |
971 | ||
972 | * Only chmod when the revisions.log file is first created | |
973 | ||
974 | * Make UPPERCASE variables work | |
975 | ||
976 | * Added rails_env variable (defaults to production) for use by tasks that employ the RAILS_ENV environment variable | |
977 | ||
978 | * Added Actor.default_io_proc | |
979 | ||
980 | * Set :actor key on SSH channel instances | |
981 | ||
982 | ||
983 | ## 0.10.0 / January 2nd, 2006 | |
984 | ||
985 | * Handle ssh password prompts like "someone's password:" | |
986 | ||
987 | * Make CLI#echo available as a class method. | |
988 | ||
989 | * Add CLI#with_echo. | |
990 | ||
991 | * Make the default password prompt available as a class method. | |
992 | ||
993 | # Add documentation for the CLI class. | |
994 | ||
995 | * Add a sanity check to make sure the correct versions of Net::SSH and Net::SFTP are installed. | |
996 | ||
997 | * Added a cleanup task to remove unused releases from the deployment directory | |
998 | ||
999 | * Allow password to be reentered on sudo if it was entered incorrectly | |
1000 | ||
1001 | * Use && as the command separator for the checkouts, so that errors are caught early. | |
1002 | ||
1003 | * Ping each SSH connection every 1s during command processing so that long-running commands don't cause the connection to timeout. | |
1004 | ||
1005 | * Add a 0.01s sleep during the command loop so that the CPU doesn't go ballistic while ST is doing its thing. | |
1006 | ||
1007 | * Add :restart_via variable for specifying whether restart ought to use :sudo (default, use sudo) | |
1008 | ||
1009 | * Use SFTP for file transfers (if available). | |
1010 | ||
1011 | * Add an "update_current" task that will do an svn up on the current release | |
1012 | ||
1013 | * Use the :checkout variable to determine what operation to use for svn checkouts (instead of co, like "export"). | |
1014 | ||
1015 | * The Rails rake tasks now load ST directly, instead of invoking it via system | |
1016 | ||
1017 | * Added ssh_options variable to configure the SSH connection parameters #2734 [jerrett@bravenet.com] | |
1018 | ||
1019 | * Require Net::SSH 1.0.5 | |
1020 | ||
1021 | ||
1022 | ## 0.9.0 / October 18th, 2005 | |
1023 | ||
1024 | * Use process reaper instead of custom reap script for restarting | |
1025 | ||
1026 | * Use -S switch to set variables before reading recipe files #2242 | |
1027 | ||
1028 | * Have setup.rb create a switchtower.cmd file on Win32 platforms #2402 | |
1029 | ||
1030 | * Add diff_from_last_deploy to the rails switchtower rakefile template | |
1031 | ||
1032 | * Add diff_from_last_deploy task (currently only works with subversion) | |
1033 | ||
1034 | * Add deploy_with_migrations task. | |
1035 | ||
1036 | * Make the migrate task more customizable. | |
1037 | ||
1038 | * If no password is given with the -p switch, prompt for password immediately. | |
1039 | ||
1040 | * Do not install a switchtower stub in the script directory. Assume the switchtower executable is in the path. | |
1041 | ||
1042 | * Remove trailing newlines from commands to prevent trailing backslash #2141 | |
1043 | ||
1044 | * Default parameters work correctly with the generator #2218 [Scott Barron] | |
1045 | ||
1046 | * Attempt to require 'rubygems' explicitly when running the switchtower utility #2134 | |
1047 | ||
1048 | * Make default tasks work only on app/db/web roles, so that additional roles may be created for boxes with specific needs without needing to (for instance) deploy the app to those boxes | |
1049 | ||
1050 | * Default the application name to "Application" when using --apply-to | |
1051 | ||
1052 | * Show the help screen instead of an error when no arguments are given | |
1053 | ||
1054 | * Make SwitchTower easier to invoke programmatically via SwitchTower::CLI | |
1055 | ||
1056 | * Specify the revision to release via the :revision variable (defaults to latest revision) | |
1057 | ||
1058 | * Allow variables to be set via the cli using the -s switch | |
1059 | ||
1060 | * Log checkouts to a "revisions.log" file | |
1061 | ||
1062 | * Changed behavior of checkout to use the timestamp as the release name, instead of the revision number | |
1063 | ||
1064 | * Added CVS module (very very experimental!) | |
1065 | ||
1066 | * Works with public keys now, for passwordless deployment | |
1067 | ||
1068 | * Subversion module recognizes the password prompt for HTTP authentication | |
1069 | ||
1070 | * Preserve +x on scripts when using darcs #1929 [Scott Barron] | |
1071 | ||
1072 | * When executing multiline commands, use a backslash to escape the newline |
0 | # Capistrano 3.x Changelog | |
1 | ||
2 | Reverse Chronological Order: | |
3 | ||
4 | ## master | |
5 | ||
6 | https://github.com/capistrano/capistrano/compare/v3.2.0...HEAD | |
7 | ||
8 | ## `3.2.0`` | |
9 | ||
10 | The changelog entries here are incomplete, because many authors choose not to | |
11 | be credited for their work, check the tag comparison link for Github. | |
12 | ||
13 | https://github.com/capistrano/capistrano/compare/v3.1.0...v3.2.0 | |
14 | ||
15 | * Minor changes: | |
16 | * Added `keys` method to Server properties to allow introspection of automatically added | |
17 | properties. | |
18 | * Compatibility with Rake 10.2.0 - `ensure_task` is now added to `@top_level_tasks` as a string. (@dmarkow) | |
19 | * Amended the git check command, "ls-remote", to use "-h", limiting the list to refs/heads | |
20 | ||
21 | ## `3.1.0` | |
22 | ||
23 | https://github.com/capistrano/capistrano/compare/v3.0.1...v3.1.0 | |
24 | ||
25 | Breaking changes: | |
26 | ||
27 | * `deploy:restart` task **is no longer run by default**. | |
28 | From this version, developers who restart the app on each deploy need to declare it in their deploy flow (eg `after 'deploy:publishing', 'deploy:restart'`). | |
29 | ||
30 | Please, check https://github.com/capistrano/capistrano/commit/4e6523e1f50707499cf75eb53dce37a89528a9b0 for more information. (@kirs) | |
31 | ||
32 | * Minor changes | |
33 | * Tasks that used `linked_dirs` and `linked_files` now run on all roles, not just app roles (@mikespokefire) | |
34 | * Tasks `deploy:linked_dirs`, `deploy:make_linked_dirs`, `deploy:linked_files`, `deploy:cleanup_rollback`, | |
35 | `deploy:log_revision` and `deploy:revert_release` now use `release_roles()` not `roles()` meaning that they | |
36 | will only run on servers where the `no_release` property is not falsy. (@leehambley) | |
37 | * Fixed bug when `deploy:cleanup` was executed twice by default (@kirs) | |
38 | * Config location can now be changed with `deploy_config_path` and `stage_config_path` options (@seenmyfate) | |
39 | * `no_release` option is now available (@seenmyfate) | |
40 | * Raise an error if developer tries to define `:all` role, which is reserved (@kirs) | |
41 | * `deploy:failed` hook was added to add some custom behaviour on failed deploy (@seenmyfate) | |
42 | * Correctly infer namespace in task enhancements (@seenmyfate) | |
43 | * Add SHA to revision log (@blackxored) | |
44 | * Allow configuration of multiple servers with same hostname but different ports (@rsslldnphy) | |
45 | * Add command line option to control role filtering (@andytinycat) | |
46 | * Make use of recent changes in Rake to over-ride the application name (@shime) | |
47 | * Readme corrections (@nathanstitt) | |
48 | * Allow roles to be fetched with a variable containing an array (@seenmyfate) | |
49 | * Improve console (@jage) | |
50 | * Add ability to filter tasks to specific servers (host filtering). (@andytinycat) | |
51 | * Add a command line option to control role filter (`--roles`) (@andytinycat) | |
52 | * Use an SCM object with a pluggable strategy (@coffeeaddict) | |
53 | ||
54 | Big thanks to @Kriechi for his help. | |
55 | ||
56 | ## `3.0.1` | |
57 | ||
58 | https://github.com/capistrano/capistrano/compare/v3.0.0...v3.0.1 | |
59 | ||
60 | * `capify` not listed as executable (@leehambley) | |
61 | * Confirm license as MIT (@leehambley) | |
62 | * Move the git ssh helper to application path (@mpapis) | |
63 | ||
64 | ## `3.0.0` | |
65 | ||
66 | https://github.com/capistrano/capistrano/compare/2.15.5...v3.0.0 | |
67 | ||
68 | If you are coming here to wonder why your Capfile doesn't work anymore, please | |
69 | vendor lock your Capistrano at 2.x, whichever version was working for you | |
70 | until today. | |
71 | ||
72 | Capistrano 3 is a ground-up rewrite with modularity, stability, speed and | |
73 | future proofing in mind. It's a big change, but now the code is 10x smaller, | |
74 | runs faster, is easier to read, and quicker to extend. In the reduction we've | |
75 | come up with a great gem based modular system for plugins and we're really | |
76 | proud of this release. | |
77 | ||
78 | The 3.0.0 release contains 38 patches from the following amazing people: | |
79 | ||
80 | * Tom `seenmyfate` Clements: more than 28 patches including cucumber integration tests! Not to | |
81 | mention Rails asset pipeline code, and bundler integrations. | |
82 | * Lee Hambley: Small changes around compatibility and log formatting | |
83 | * Kir Shatrov: for improvements in the core to make it easier to write extensions, for | |
84 | improving documentation, and for effectively building the chruby, rvm and rbenv integrations. | |
85 | * Michael Nikitochkin: Fixing a bug around linked files and directories. | |
86 | * Jack Thorne: for improvements to the default `Capfile` to fix some bad example syntax. | |
87 | * Erik Hetzner: for (what looks like great) work on the Mercurial (Hg) support. The Hg and Git | |
88 | source control mechanisms do not work the same way, but rather lean on the strengths of the | |
89 | underlying tools. | |
90 | ||
91 | (If I missed anyone, I'm sorry, your contributions have been awesome) | |
92 | ||
93 | The 2.x branch of code is now no longer maintained. Towards the end of it's | |
94 | useful life there were an increasing number of features and pieces of code | |
95 | which didn't make sense for certain groups of people, in certain situations, | |
96 | leading a to a ping-pong tennis effect with pull requests every few weeks | |
97 | "fixing" a use-case which had already been "fixed" shortly before. As many of | |
98 | the use-cases are outside the scope of the testing environments I (and by | |
99 | extension the trusted contributors and IRC regulars) were able to test for. | |
100 | ||
101 | There's a more extensive post about my failure to be able to keep up with the | |
102 | demands of maintaining v2 whilst trying to build something which is appropriate | |
103 | for the current landscape. If you are affected by the unsupported 2 branch, | |
104 | please contact me (Lee Hambley) to discuss how my company can help support you. | |
105 | Otherwise, please try v3, we're sure you'll like it, and the code is designed | |
106 | to be so simple that anyone can work on it. | |
107 | ||
108 | ## `3.0.0.pre14` | |
109 | ||
110 | * Thanks to numerous contributors, in particular (@teohm) for a series of improvements. | |
111 | ||
112 | ## `3.0.0.pre13` | |
113 | ||
114 | * Fixed typos in the Capfile. (@teohm) | |
115 | * Allow setting SSH options globally. (@korin) | |
116 | * Change the flow (and hooks) see http://www.capistranorb.com/documentation/getting-started/flow/ for more information. Requires min SSHKit 0.0.34 (@teohm) | |
117 | * Fix sorting releases in lexicographical order (@teohm) | |
118 | ||
119 | ## `3.0.0.pre12` | |
120 | ||
121 | * `capistrano/bundler` now runs bundle on all roles, this addresses the same | |
122 | issue as the related changes in `pre11`. (@leehambley) | |
123 | ||
124 | ## `3.0.0.pre11` | |
125 | ||
126 | * Some deploy.rake tasks now apply to all servers, not expecting a | |
127 | primary(:app) server which may not exist in all deploy environments. | |
128 | (@leehambley). | |
129 | ||
130 | ## `3.0.0.pre10` | |
131 | ||
132 | * Fixes pre9. | |
133 | ||
134 | ## `3.0.0.pre9` | |
135 | ||
136 | * Fixes a syntax error introduced with filtering (with tests) introduced in | |
137 | `pre8`. (@leehambley) | |
138 | ||
139 | ## `3.0.0.pre8` | |
140 | ||
141 | * Fixed a syntax where `roles(:foo, :bar)` was being mistaken as a filter | |
142 | (roles(:foo, :bar => nil). The correct syntax to use is: roles([:foo,:bar]) | |
143 | (@leehambley) | |
144 | ||
145 | ## `3.0.0.pre7` | |
146 | ||
147 | * Fix Git https authentication. (@leehambley) | |
148 | * Capfile template fixes (repo/repo_url) (@teohm) | |
149 | * Readme Fixes (@ffmike, @kejadlen, @dwickwire) | |
150 | * Fix the positioning of the bundler hook, now immediately after finalize. (@teohm) |
0 | ## CONTRIBUTING | |
1 | ||
2 | **The issue tracker is intended exclusively for things that are genuine bugs, | |
3 | or improvements to the code.** | |
4 | ||
5 | If you have a user support query, or you suspect that you might just be holding | |
6 | it wrong, drop us a line at [the mailing list](https://groups.google.com/forum/#!forum/capistrano), [StackOverflow](http://stackoverflow.com/questions/tagged/capistrano) or at [CodersClan](http://codersclan.net/?repo_id=325&source=contributing). The | |
7 | mailing list is moderated to cut down on spam, so please be patient, if you use | |
8 | StackOverflow, make sure to tag your post with "Capistrano". (Not forgetting | |
9 | any other tags which might relate, rvm, rbenv, Ubuntu, etc.) | |
10 | ||
11 | If you have an urgent problem you can use [CodersClan](http://codersclan.net/?repo_id=325&source=contributing) to solve your problem quickly. CodersClan has a community of Capistrano experts dedicated to solve code problems for bounties. | |
12 | ||
13 | Wherever you post please be sure to include the version of Capistrano you are | |
14 | using, which versions of any plugins (*capistrano-rvm*, *capistrano-bundler*, | |
15 | etc.). Proper logs are vital, if you need to redact them, go ahead, but be | |
16 | careful not to remove anything important. Please take care to format logs and | |
17 | code correctly, ideally wrapped to a sane line length, and in a mono spaced | |
18 | font. This all helps us to gather a clear understanding of what is going wrong. | |
19 | ||
20 | **If you really think that you found a bug, or want to enquire about a feature, | |
21 | or send us a patch to add a feature, or fix a bug, please keep a few things in | |
22 | mind:** | |
23 | ||
24 | ## When Submitting An Issue: | |
25 | ||
26 | If you think there's a bug, please make sure it's really a bug in Capistrano. | |
27 | As Capistrano sits on the (sometimes rough) edges between SSH, Git, the | |
28 | network, Ruby, RVM, rbenv, chruby, Bundler, your Linux distribution, countless | |
29 | shell configuration files on your end, and the server… there's a good chance | |
30 | the problem lies somewhere else. | |
31 | ||
32 | Please make sure you have reviewed the FAQs at http://www.capistranorb.com/. | |
33 | ||
34 | It's really important to include as much information as possible, versions of | |
35 | everything involved, anything weird you might be doing that might be having | |
36 | side effects, include as much as you can in a [GitHub | |
37 | Gist](https://gist.github.com/) and link that from the issue, with tools such | |
38 | as Gist, we can link to individual lines and help work out what is going wrong. | |
39 | ||
40 | If you are an experienced Ruby programmer, take a few minutes to get our test | |
41 | suite running, and do what you can to get a test case written that fails, from | |
42 | there we can understand exactly what it takes to reproduce the issue (as it's | |
43 | documented with code) | |
44 | ||
45 | ## When Requesting a Feature: | |
46 | ||
47 | We can't make everyone happy all of the time, and we've been around the block | |
48 | well enough to know when something doesn't work well, or when your proposed fix | |
49 | might impact other things. | |
50 | ||
51 | We prefer to [start with | |
52 | "no"](https://gettingreal.37signals.com/ch05_Start_With_No.php), and help you | |
53 | find a better way to solve your problem, sometimes the solution is to [build | |
54 | faster | |
55 | horses](http://blog.cauvin.org/2010/07/henry-fords-faster-horse-quote.html), | |
56 | sometimes the solution is to work around it in a neat way that you didn't know | |
57 | existed. | |
58 | ||
59 | Please don't be offended if we say no, and don't be afraid to fight your | |
60 | corner, try and avoid being one of the [poisonous | |
61 | people](https://www.youtube.com/watch?v=Q52kFL8zVoM) | |
62 | ||
63 | ## Submitting A Pull Request: | |
64 | ||
65 | Pull requests are awesome, and if they arrive with decent tests, and conform to | |
66 | the guidelines below, we'll merge them in as soon as possible, we'll let you | |
67 | know which release we're planning them for (we adhere to | |
68 | [semver](http://semver.org/) so please don't be upset if we plan your changes | |
69 | for a later release) | |
70 | ||
71 | * The code is MIT licenced, your code will fall under the same license if we merge it. | |
72 | * We can't merge it without a [good commit | |
73 | message](http://robots.thoughtbot.com/5-useful-tips-for-a-better-commit-message). | |
74 | If you do this right, Github will use the commit message as the body of your | |
75 | pull request, double win. | |
76 | * If you are referencing an improvement to an existing issue (if we have not | |
77 | yet merged it ) | |
78 | * Add an entry to the `CHANGELOG` under the `### master` section, but please | |
79 | don't mess with the version. | |
80 | * If you add a new feature, please make sure to document it, open a | |
81 | corresponding pull request in [the | |
82 | documentation](https://github.com/capistrano/documentation) and mention the | |
83 | code change pull request over there, and Github will link everything up. If | |
84 | it's a simple feature, or a new variable, or something changed, it may be | |
85 | appropriate simply to document it in the generated `Capfile` or `deploy.rb`, or | |
86 | in the `README` | |
87 | * Take care to squash your commit into one single commit with a good message, it | |
88 | saves us a lot of work in maintaining the CHANGELOG if we can generate it from | |
89 | the commit messages between the release tags! | |
90 | * Tests! It's tricky to test some parts of Capistrano, but do your best, it | |
91 | might just serve as a starting point for us to build a reliable test on top of, | |
92 | and help us understand where you are coming from. |
0 | source "http://rubygems.org" | |
0 | source 'https://rubygems.org' | |
1 | 1 | |
2 | 2 | # Specify your gem's dependencies in capistrano.gemspec |
3 | 3 | gemspec |
4 | 4 | |
5 | # | |
6 | # Development Dependencies from the Gemfile | |
7 | # are merged here. | |
8 | # | |
9 | group :development do | |
10 | gem 'rake', '0.8.7' | |
5 | group :cucumber do | |
6 | gem 'kuroko' | |
7 | gem 'cucumber' | |
11 | 8 | end |
9 | ||
10 | platforms :rbx do | |
11 | gem 'rubysl', '~> 2.0' | |
12 | end |
0 | MIT License (MIT) | |
1 | ||
2 | Copyright (c) 2012-2013 Tom Clements, Lee Hambley | |
3 | ||
4 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
5 | of this software and associated documentation files (the "Software"), to deal | |
6 | in the Software without restriction, including without limitation the rights | |
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
8 | copies of the Software, and to permit persons to whom the Software is | |
9 | furnished to do so, subject to the following conditions: | |
10 | ||
11 | The above copyright notice and this permission notice shall be included in | |
12 | all copies or substantial portions of the Software. | |
13 | ||
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
20 | THE SOFTWARE. |
0 | # Capistrano [![Build Status](https://travis-ci.org/capistrano/capistrano.png?branch=v3)](https://travis-ci.org/capistrano/capistrano) [![Code Climate](https://codeclimate.com/github/capistrano/capistrano.png)](https://codeclimate.com/github/capistrano/capistrano) <a href="http://codersclan.net/?repo_id=325&source=small"><img src="http://www.codersclan.net/gs_button/?repo_id=325&size=small" width="69"></a> | |
1 | ||
2 | ## Requirements | |
3 | ||
4 | * Ruby >= 1.9 (JRuby and C-Ruby/YARV are supported) | |
5 | ||
6 | ## Support | |
7 | ||
8 | Need help with getting Capistrano up and running? Got a code problem you want to get solved quickly? | |
9 | ||
10 | Get <a href="http://codersclan.net/?repo_id=325&source=link">Capistrano support on CodersClan.</a> | |
11 | ||
12 | <a href="http://codersclan.net/?repo_id=325&source=big"><img src="http://www.codersclan.net/gs_button/?repo_id=325" width="200"></a> | |
13 | ||
14 | ## Installation | |
15 | ||
16 | Add this line to your application's Gemfile: | |
17 | ||
18 | ``` ruby | |
19 | gem 'capistrano', '~> 3.1.0' | |
20 | ``` | |
21 | ||
22 | And then execute: | |
23 | ||
24 | ``` sh | |
25 | $ bundle install | |
26 | ``` | |
27 | ||
28 | Capify: | |
29 | *make sure there's no "Capfile" or "capfile" present* | |
30 | ``` sh | |
31 | $ bundle exec cap install | |
32 | ``` | |
33 | ||
34 | This creates the following files: | |
35 | ||
36 | ``` | |
37 | ├── Capfile | |
38 | ├── config | |
39 | │ ├── deploy | |
40 | │ │ ├── production.rb | |
41 | │ │ └── staging.rb | |
42 | │ └── deploy.rb | |
43 | └── lib | |
44 | └── capistrano | |
45 | └── tasks | |
46 | ``` | |
47 | ||
48 | To create different stages: | |
49 | ||
50 | ``` sh | |
51 | $ bundle exec cap install STAGES=local,sandbox,qa,production | |
52 | ``` | |
53 | ||
54 | ## Usage | |
55 | ||
56 | ``` sh | |
57 | $ bundle exec cap -vT | |
58 | ||
59 | $ bundle exec cap staging deploy | |
60 | $ bundle exec cap production deploy | |
61 | ||
62 | $ bundle exec cap production deploy --dry-run | |
63 | $ bundle exec cap production deploy --prereqs | |
64 | $ bundle exec cap production deploy --trace | |
65 | ``` | |
66 | ||
67 | ## Tasks | |
68 | ||
69 | ``` ruby | |
70 | server 'example.com', roles: [:web, :app] | |
71 | server 'example.org', roles: [:db, :workers] | |
72 | desc "Report Uptimes" | |
73 | task :uptime do | |
74 | on roles(:all) do |host| | |
75 | execute :any_command, "with args", :here, "and here" | |
76 | info "Host #{host} (#{host.roles.to_a.join(', ')}):\t#{capture(:uptime)}" | |
77 | end | |
78 | end | |
79 | ``` | |
80 | ||
81 | **Note**: | |
82 | ||
83 | **tl;dr**: `execute(:bundle, :install)` and `execute('bundle install')` don't behave identically! | |
84 | ||
85 | `execute()` has a subtle behaviour. When calling `within './directory' { execute(:bundle, :install) }` for example, the first argument to `execute()` is a *Stringish* with ***no whitespace***. This allows the command to pass through the [SSHKit::CommandMap](https://github.com/capistrano/sshkit#the-command-map) which enables a number of powerful features. | |
86 | ||
87 | When the first argument to `execute()` contains whitespace, for example `within './directory' { execute('bundle install') }` (or when using a heredoc), neither Capistrano, nor SSHKit can reliably predict how it should be shell escaped, and thus cannot perform any context, or command mapping, that means that the `within(){}` (as well as `with()`, `as()`, etc) have no effect. There have been a few attempts to resolve this, but we don't consider it a bug although we acknowledge that it might be a little counter intuitive. | |
88 | ## Before / After | |
89 | ||
90 | Where calling on the same task name, executed in order of inclusion | |
91 | ||
92 | ``` ruby | |
93 | # call an existing task | |
94 | before :starting, :ensure_user | |
95 | ||
96 | after :finishing, :notify | |
97 | ||
98 | ||
99 | # or define in block | |
100 | before :starting, :ensure_user do | |
101 | # | |
102 | end | |
103 | ||
104 | after :finishing, :notify do | |
105 | # | |
106 | end | |
107 | ``` | |
108 | ||
109 | If it makes sense for your use case (often, that means *generating a file*) | |
110 | the Rake prerequisite mechanism can be used: | |
111 | ||
112 | ``` ruby | |
113 | desc "Create Important File" | |
114 | file 'important.txt' do |t| | |
115 | sh "touch #{t.name}" | |
116 | end | |
117 | desc "Upload Important File" | |
118 | task :upload => 'important.txt' do |t| | |
119 | on roles(:all) do | |
120 | upload!(t.prerequisites.first, '/tmp') | |
121 | end | |
122 | end | |
123 | ``` | |
124 | ||
125 | The final way to call out to other tasks is to simply `invoke()` them: | |
126 | ||
127 | ``` ruby | |
128 | namespace :example do | |
129 | task :one do | |
130 | on roles(:all) { info "One" } | |
131 | end | |
132 | task :two do | |
133 | invoke "example:one" | |
134 | on roles(:all) { info "Two" } | |
135 | end | |
136 | end | |
137 | ``` | |
138 | ||
139 | This method is widely used. | |
140 | ||
141 | ## Getting User Input | |
142 | ||
143 | ``` ruby | |
144 | desc "Ask about breakfast" | |
145 | task :breakfast do | |
146 | ask(:breakfast, "pancakes") | |
147 | on roles(:all) do |h| | |
148 | execute "echo \"$(whoami) wants #{fetch(:breakfast)} for breakfast!\"" | |
149 | end | |
150 | end | |
151 | ``` | |
152 | ||
153 | Perfect, who needs telephones. | |
154 | ||
155 | ||
156 | ## Using password authentication | |
157 | ||
158 | Password authentication can be done via `set` and `ask` in your deploy environment file (e.g.: config/environments/production.rb) | |
159 | ||
160 | ```ruby | |
161 | set :password, ask('Server password:', nil) | |
162 | server 'server.domain.com', user: 'ssh_user_name', port: 22, password: fetch(:password), roles: %w{web app db} | |
163 | ``` | |
164 | ||
165 | ## Running local tasks | |
166 | ||
167 | Local tasks can be run by replacing `on` with `run_locally` | |
168 | ||
169 | ``` ruby | |
170 | desc 'Notify service of deployment' | |
171 | task :notify do | |
172 | run_locally do | |
173 | with rails_env: :development do | |
174 | rake 'service:notify' | |
175 | end | |
176 | end | |
177 | end | |
178 | ``` | |
179 | ||
180 | Of course, you can always just use standard ruby syntax to run things locally | |
181 | ``` ruby | |
182 | desc 'Notify service of deployment' | |
183 | task :notify do | |
184 | %x('RAILS_ENV=development bundle exec rake "service:notify"') | |
185 | end | |
186 | ``` | |
187 | ||
188 | Alternatively you could use the rake syntax | |
189 | ``` ruby | |
190 | desc "Notify service of deployment" | |
191 | task :notify do | |
192 | sh 'RAILS_ENV=development bundle exec rake "service:notify"' | |
193 | end | |
194 | ``` | |
195 | ## Console | |
196 | ||
197 | **Note:** Here be dragons. The console is very immature, but it's much more | |
198 | cleanly architected than previous incarnations and it'll only get better from | |
199 | here on in. | |
200 | ||
201 | Execute arbitrary remote commands, to use this simply add | |
202 | `require 'capistrano/console'` which will add the necessary tasks to your | |
203 | environment: | |
204 | ||
205 | ``` sh | |
206 | $ bundle exec cap staging console | |
207 | ``` | |
208 | ||
209 | Then, after setting up the server connections, this is how that might look: | |
210 | ||
211 | ``` sh | |
212 | $ bundle exec cap production console | |
213 | capistrano console - enter command to execute on production | |
214 | production> uptime | |
215 | INFO [94db8027] Running /usr/bin/env uptime on leehambley@example.com:22 | |
216 | DEBUG [94db8027] Command: /usr/bin/env uptime | |
217 | DEBUG [94db8027] 17:11:17 up 50 days, 22:31, 1 user, load average: 0.02, 0.02, 0.05 | |
218 | INFO [94db8027] Finished in 0.435 seconds command successful. | |
219 | production> who | |
220 | INFO [9ce34809] Running /usr/bin/env who on leehambley@example.com:22 | |
221 | DEBUG [9ce34809] Command: /usr/bin/env who | |
222 | DEBUG [9ce34809] leehambley pts/0 2013-06-13 17:11 (port-11262.pppoe.wtnet.de) | |
223 | INFO [9ce34809] Finished in 0.420 seconds command successful. | |
224 | ``` | |
225 | ||
226 | ## A word about PTYs | |
227 | ||
228 | There is a configuration option which asks the backend driver to ask the remote host | |
229 | to assign the connection a *pty*. A *pty* is a pseudo-terminal, which in effect means | |
230 | *tell the backend that this is an __interactive__ session*. This is normally a bad idea. | |
231 | ||
232 | Most of the differences are best explained by [this page](https://github.com/sstephenson/rbenv/wiki/Unix-shell-initialization) from the author of *rbenv*. | |
233 | ||
234 | **When Capistrano makes a connection it is a *non-login*, *non-interactive* shell. | |
235 | This was not an accident!** | |
236 | ||
237 | It's often used as a band aid to cure issues related to RVM and rbenv not loading login | |
238 | and shell initialisation scripts. In these scenarios RVM and rbenv are the tools at fault, | |
239 | or at least they are being used incorrectly. | |
240 | ||
241 | Whilst, especially in the case of language runtimes (Ruby, Node, Python and friends in | |
242 | particular) there is a temptation to run multiple versions in parallel on a single server | |
243 | and to switch between them using environmental variables, this is an anti-pattern, and | |
244 | symptomatic of bad design (e.g. you're testing a second version of Ruby in production because | |
245 | your company lacks the infrastructure to test this in a staging environment). | |
246 | ||
247 | ## Configuration | |
248 | ||
249 | The following variables are settable: | |
250 | ||
251 | | Variable Name | Description | Notes | | |
252 | |:---------------------:|----------------------------------------------------------------------|-----------------------------------------------------------------| | |
253 | | `:repo_url` | The URL of your scm repository (git, hg, svn) | file://, https://, ssh://, or svn+ssh:// are all supported | | |
254 | | `:branch` | The branch you wish to deploy | This only has meaning for git and hg repos, to specify the branch of an svn repo, set `:repo_url` to the branch location. | | |
255 | | `:scm` | The source control system used | `:git`, `:hg`, `:svn` are currently supported | | |
256 | | `:tmp_dir` | The (optional) temp directory that will be used (default: /tmp) | if you have a shared web host, this setting may need to be set (i.e. /home/user/tmp/capistrano). | | |
257 | ||
258 | __Support removed__ for following variables: | |
259 | ||
260 | | Variable Name | Description | Notes | | |
261 | |:---------------------:|---------------------------------------------------------------------|-----------------------------------------------------------------| | |
262 | | `:copy_exclude` | The (optional) array of files and/or folders excluded from deploy | Replaced by Git's native `.gitattributes`, see [#515](https://github.com/capistrano/capistrano/issues/515) for more info. | | |
263 | ||
264 | ## SSHKit | |
265 | ||
266 | [SSHKit](https://github.com/leehambley/sshkit) is the driver for SSH | |
267 | connections behind the scenes in Capistrano. Depending on how deep you dig, you | |
268 | might run into interfaces that come directly from SSHKit (the configuration is | |
269 | a good example). | |
270 | ||
271 | ## License | |
272 | ||
273 | MIT License (MIT) | |
274 | ||
275 | Copyright (c) 2012-2013 Tom Clements, Lee Hambley | |
276 | ||
277 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
278 | of this software and associated documentation files (the "Software"), to deal | |
279 | in the Software without restriction, including without limitation the rights | |
280 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
281 | copies of the Software, and to permit persons to whom the Software is | |
282 | furnished to do so, subject to the following conditions: | |
283 | ||
284 | The above copyright notice and this permission notice shall be included in | |
285 | all copies or substantial portions of the Software. | |
286 | ||
287 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
288 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
289 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
290 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
291 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
292 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
293 | THE SOFTWARE. |
0 | ## Capistrano | |
1 | ||
2 | [![Build | |
3 | Status](https://secure.travis-ci.org/capistrano/capistrano.png)](http://travis-ci.org/capistrano/capistrano) | |
4 | ||
5 | ||
6 | Capistrano is a utility and framework for executing commands in parallel on | |
7 | multiple remote machines, via SSH. It uses a simple DSL (borrowed in part from | |
8 | [Rake](http://rake.rubyforge.org/)) that allows you to define _tasks_, which may | |
9 | be applied to machines in certain roles. It also supports tunneling connections | |
10 | via some gateway machine to allow operations to be performed behind VPN's and | |
11 | firewalls. | |
12 | ||
13 | Capistrano was originally designed to simplify and automate deployment of web | |
14 | applications to distributed environments, and originally came bundled with a set | |
15 | of tasks designed for deploying Rails applications. | |
16 | ||
17 | ## Documentation | |
18 | ||
19 | * [http://github.com/capistrano/capistrano/wiki/Documentation-v2.x](http://github.com/capistrano/capistrano/wiki/Documentation-v2.x) | |
20 | ||
21 | ## DEPENDENCIES | |
22 | ||
23 | * [Net::SSH](http://net-ssh.rubyforge.org) | |
24 | * [Net::SFTP](http://net-ssh.rubyforge.org) | |
25 | * [Net::SCP](http://net-ssh.rubyforge.org) | |
26 | * [Net::SSH::Gateway](http://net-ssh.rubyforge.org) | |
27 | * [HighLine](http://highline.rubyforge.org) | |
28 | ||
29 | If you want to run the tests, you'll also need to install the dependencies with | |
30 | Bundler, see the `Gemfile` within . | |
31 | ||
32 | ## ASSUMPTIONS | |
33 | ||
34 | Capistrano is "opinionated software", which means it has very firm ideas about | |
35 | how things ought to be done, and tries to force those ideas on you. Some of the | |
36 | assumptions behind these opinions are: | |
37 | ||
38 | * You are using SSH to access the remote servers. | |
39 | * You either have the same password to all target machines, or you have public | |
40 | keys in place to allow passwordless access to them. | |
41 | ||
42 | Do not expect these assumptions to change. | |
43 | ||
44 | ## USAGE | |
45 | ||
46 | In general, you'll use Capistrano as follows: | |
47 | ||
48 | * Create a recipe file ("capfile" or "Capfile"). | |
49 | * Use the `cap` script to execute your recipe. | |
50 | ||
51 | Use the `cap` script as follows: | |
52 | ||
53 | cap sometask | |
54 | ||
55 | By default, the script will look for a file called one of `capfile` or | |
56 | `Capfile`. The `someaction` text indicates which task to execute. You can do | |
57 | "cap -h" to see all the available options and "cap -T" to see all the available | |
58 | tasks. | |
59 | ||
60 | ## CONTRIBUTING: | |
61 | ||
62 | * Fork Capistrano | |
63 | * Create a topic branch - `git checkout -b my_branch` | |
64 | * Rebase your branch so that all your changes are reflected in one | |
65 | commit | |
66 | * Push to your branch - `git push origin my_branch` | |
67 | * Create a Pull Request from your branch, include as much documentation | |
68 | as you can in the commit message/pull request, following these | |
69 | [guidelines on writing a good commit message](http://spheredev.org/wiki/Git_for_the_lazy#Writing_good_commit_messages) | |
70 | * That's it! | |
71 | ||
72 | ||
73 | ## LICENSE: | |
74 | ||
75 | Permission is hereby granted, free of charge, to any person obtaining | |
76 | a copy of this software and associated documentation files (the | |
77 | 'Software'), to deal in the Software without restriction, including | |
78 | without limitation the rights to use, copy, modify, merge, publish, | |
79 | distribute, sublicense, and/or sell copies of the Software, and to | |
80 | permit persons to whom the Software is furnished to do so, subject to | |
81 | the following conditions: | |
82 | ||
83 | The above copyright notice and this permission notice shall be | |
84 | included in all copies or substantial portions of the Software. | |
85 | ||
86 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, | |
87 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
88 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |
89 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY | |
90 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, | |
91 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE | |
92 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
0 | require 'bundler' | |
1 | Bundler::GemHelper.install_tasks | |
0 | require "bundler/gem_tasks" | |
2 | 1 | |
3 | require 'rake/testtask' | |
4 | Rake::TestTask.new(:test) do |test| | |
5 | test.libs << 'lib' << 'test' | |
6 | test.pattern = 'test/**/*_test.rb' | |
7 | test.verbose = true | |
8 | end | |
9 | ||
10 | task :default => :test | |
2 | task :default => :spec | |
3 | require 'rspec/core/rake_task' | |
4 | RSpec::Core::RakeTask.new |
0 | 0 | #!/usr/bin/env ruby |
1 | ||
2 | require 'capistrano/cli' | |
3 | Capistrano::CLI.execute | |
1 | require 'capistrano/all' | |
2 | Capistrano::Application.new.run |
0 | 0 | #!/usr/bin/env ruby |
1 | ||
2 | require 'optparse' | |
3 | require 'fileutils' | |
4 | ||
5 | OptionParser.new do |opts| | |
6 | opts.banner = "Usage: #{File.basename($0)} [path]" | |
7 | ||
8 | opts.on("-h", "--help", "Displays this help info") do | |
9 | puts opts | |
10 | exit 0 | |
11 | end | |
12 | ||
13 | begin | |
14 | opts.parse!(ARGV) | |
15 | rescue OptionParser::ParseError => e | |
16 | warn e.message | |
17 | puts opts | |
18 | exit 1 | |
19 | end | |
20 | end | |
21 | ||
22 | if ARGV.empty? | |
23 | abort "Please specify the directory to capify, e.g. `#{File.basename($0)} .'" | |
24 | elsif !File.exists?(ARGV.first) | |
25 | abort "`#{ARGV.first}' does not exist." | |
26 | elsif !File.directory?(ARGV.first) | |
27 | abort "`#{ARGV.first}' is not a directory." | |
28 | elsif ARGV.length > 1 | |
29 | abort "Too many arguments; please specify only the directory to capify." | |
30 | end | |
31 | ||
32 | def unindent(string) | |
33 | indentation = string[/\A\s*/] | |
34 | string.strip.gsub(/^#{indentation}/, "") | |
35 | end | |
36 | ||
37 | files = { | |
38 | "Capfile" => unindent(<<-FILE), | |
39 | ||
40 | load 'deploy' | |
41 | ||
42 | # Uncomment if you are using Rails' asset pipeline | |
43 | # load 'deploy/assets' | |
44 | ||
45 | Dir['vendor/gems/*/recipes/*.rb','vendor/plugins/*/recipes/*.rb'].each { |plugin| load(plugin) } | |
46 | ||
47 | load 'config/deploy' # remove this line to skip loading any of the default tasks | |
48 | FILE | |
49 | ||
50 | "config/deploy.rb" => 'set :application, "set your application name here" | |
51 | set :repository, "set your repository location here" | |
52 | ||
53 | set :scm, :subversion | |
54 | # Or: `accurev`, `bzr`, `cvs`, `darcs`, `git`, `mercurial`, `perforce`, `subversion` or `none` | |
55 | ||
56 | role :web, "your web-server here" # Your HTTP server, Apache/etc | |
57 | role :app, "your app-server here" # This may be the same as your `Web` server | |
58 | role :db, "your primary db-server here", :primary => true # This is where Rails migrations will run | |
59 | role :db, "your slave db-server here" | |
60 | ||
61 | # if you want to clean up old releases on each deploy uncomment this: | |
62 | # after "deploy:restart", "deploy:cleanup" | |
63 | ||
64 | # if you\'re still using the script/reaper helper you will need | |
65 | # these http://github.com/rails/irs_process_scripts | |
66 | ||
67 | # If you are using Passenger mod_rails uncomment this: | |
68 | # namespace :deploy do | |
69 | # task :start do ; end | |
70 | # task :stop do ; end | |
71 | # task :restart, :roles => :app, :except => { :no_release => true } do | |
72 | # run "#{try_sudo} touch #{File.join(current_path,\'tmp\',\'restart.txt\')}" | |
73 | # end | |
74 | # end'} | |
75 | ||
76 | base = ARGV.shift | |
77 | files.each do |file, content| | |
78 | file = File.join(base, file) | |
79 | if File.exists?(file) | |
80 | warn "[skip] '#{file}' already exists" | |
81 | elsif File.exists?(file.downcase) | |
82 | warn "[skip] '#{file.downcase}' exists, which could conflict with `#{file}'" | |
83 | else | |
84 | unless File.exists?(File.dirname(file)) | |
85 | puts "[add] making directory '#{File.dirname(file)}'" | |
86 | FileUtils.mkdir(File.dirname(file)) | |
87 | end | |
88 | puts "[add] writing '#{file}'" | |
89 | File.open(file, "w") { |f| f.write(content) } | |
90 | end | |
91 | end | |
92 | ||
93 | puts "[done] capified!" | |
1 | puts "-" * 80 | |
2 | puts "Capistrano 3.x is incompatible with Capistrano 2.x. " | |
3 | puts | |
4 | puts "This command has become `cap install` in Capistrano 3.x" | |
5 | puts | |
6 | puts "For more information see http://www.capistranorb.com/" | |
7 | puts "-" * 80 |
0 | 0 | # -*- encoding: utf-8 -*- |
1 | $:.push File.expand_path("../lib", __FILE__) | |
2 | require "capistrano/version" | |
1 | lib = File.expand_path('../lib', __FILE__) | |
2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) | |
3 | require 'capistrano/version' | |
3 | 4 | |
4 | Gem::Specification.new do |s| | |
5 | Gem::Specification.new do |gem| | |
6 | gem.name = "capistrano" | |
7 | gem.version = Capistrano::VERSION | |
8 | gem.authors = ["Tom Clements", "Lee Hambley"] | |
9 | gem.email = ["seenmyfate@gmail.com", "lee.hambley@gmail.com"] | |
10 | gem.description = %q{Capistrano is a utility and framework for executing commands in parallel on multiple remote machines, via SSH.} | |
11 | gem.summary = %q{Capistrano - Welcome to easy deployment with Ruby over SSH} | |
12 | gem.homepage = "http://capistranorb.com/" | |
5 | 13 | |
6 | s.name = "capistrano" | |
7 | s.version = Capistrano::Version.to_s | |
8 | s.platform = Gem::Platform::RUBY | |
9 | s.authors = ["Jamis Buck", "Lee Hambley"] | |
10 | s.email = ["jamis@jamisbuck.org", "lee.hambley@gmail.com"] | |
11 | s.homepage = "http://github.com/capistrano/capistrano" | |
12 | s.summary = %q{Capistrano - Welcome to easy deployment with Ruby over SSH} | |
13 | s.description = %q{Capistrano is a utility and framework for executing commands in parallel on multiple remote machines, via SSH.} | |
14 | s.files = `git ls-files`.split("\n") | |
15 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") | |
16 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } | |
17 | s.require_paths = ["lib"] | |
18 | s.extra_rdoc_files = [ | |
19 | "README.mdown" | |
20 | ] | |
14 | gem.files = `git ls-files`.split($/) | |
15 | gem.executables = ['cap', 'capify'] | |
16 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) | |
17 | gem.require_paths = ["lib"] | |
21 | 18 | |
22 | if s.respond_to? :specification_version then | |
23 | s.specification_version = 3 | |
24 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then | |
25 | s.add_runtime_dependency(%q<highline>, [">= 0"]) | |
26 | s.add_runtime_dependency(%q<net-ssh>, [">= 2.0.14"]) | |
27 | s.add_runtime_dependency(%q<net-sftp>, [">= 2.0.0"]) | |
28 | s.add_runtime_dependency(%q<net-scp>, [">= 1.0.0"]) | |
29 | s.add_runtime_dependency(%q<net-ssh-gateway>, [">= 1.1.0"]) | |
30 | s.add_development_dependency(%q<mocha>, [">= 0"]) | |
31 | else | |
32 | s.add_dependency(%q<net-ssh>, [">= 2.0.14"]) | |
33 | s.add_dependency(%q<net-sftp>, [">= 2.0.0"]) | |
34 | s.add_dependency(%q<net-scp>, [">= 1.0.0"]) | |
35 | s.add_dependency(%q<net-ssh-gateway>, [">= 1.1.0"]) | |
36 | s.add_dependency(%q<highline>, [">= 0"]) | |
37 | s.add_dependency(%q<mocha>, [">= 0"]) | |
38 | end | |
39 | else | |
40 | s.add_dependency(%q<net-ssh>, [">= 2.0.14"]) | |
41 | s.add_dependency(%q<net-sftp>, [">= 2.0.0"]) | |
42 | s.add_dependency(%q<net-scp>, [">= 1.0.0"]) | |
43 | s.add_dependency(%q<net-ssh-gateway>, [">= 1.1.0"]) | |
44 | s.add_dependency(%q<highline>, [">= 0"]) | |
45 | s.add_dependency(%q<mocha>, [">= 0"]) | |
46 | end | |
19 | gem.licenses = ['MIT'] | |
20 | ||
21 | gem.post_install_message = <<eos | |
22 | Capistrano 3.1 has some breaking changes, like `deploy:restart` callback should be added manually to your deploy.rb. Please, check the CHANGELOG: http://goo.gl/SxB0lr | |
23 | ||
24 | If you're upgrading Capistrano from 2.x, we recommend to read the upgrade guide: http://goo.gl/4536kB | |
25 | eos | |
26 | ||
27 | gem.add_dependency 'sshkit', '~> 1.3' | |
28 | gem.add_dependency 'rake', '>= 10.0.0' | |
29 | gem.add_dependency 'i18n' | |
30 | ||
31 | gem.add_development_dependency 'rspec' | |
32 | gem.add_development_dependency 'mocha' | |
47 | 33 | end |
Binary diff not shown
0 | Feature: The path to the configuration can be changed, removing the need to | |
1 | follow Ruby/Rails conventions | |
2 | ||
3 | Background: | |
4 | Given a test app with the default configuration | |
5 | And servers with the roles app and web | |
6 | ||
7 | Scenario: Deploying with configuration in default location | |
8 | When I run "cap test" | |
9 | Then the task is successful | |
10 | ||
11 | Scenario: Deploying with configuration in a custom location | |
12 | But the configuration is in a custom location | |
13 | When I run "cap test" | |
14 | Then the task is successful | |
15 | ||
16 | Scenario: Show install task with configuration in default location | |
17 | When I run "cap -T" | |
18 | Then the task is successful | |
19 | And contains "install" in the output | |
20 | ||
21 | Scenario: Show install task with configuration in a custom location | |
22 | And config stage file has line "desc 'Special Task'" | |
23 | And config stage file has line "task :special_stage_task" | |
24 | But the configuration is in a custom location | |
25 | When I run "cap -T" | |
26 | Then the task is successful | |
27 | And contains "special_stage_task" in the output |
0 | Feature: Deploy | |
1 | ||
2 | Background: | |
3 | Given a test app with the default configuration | |
4 | And servers with the roles app and web | |
5 | ||
6 | Scenario: Creating the repo | |
7 | When I run cap "git:check" | |
8 | Then references in the remote repo are listed | |
9 | ||
10 | Scenario: Creating the directory structure | |
11 | When I run cap "deploy:check:directories" | |
12 | Then the shared path is created | |
13 | And the releases path is created | |
14 | ||
15 | Scenario: Creating linked directories | |
16 | When I run cap "deploy:check:linked_dirs" | |
17 | Then directories in :linked_dirs are created in shared | |
18 | ||
19 | Scenario: Creating linked directories for linked files | |
20 | When I run cap "deploy:check:make_linked_dirs" | |
21 | Then directories referenced in :linked_files are created in shared | |
22 | ||
23 | Scenario: Checking linked files - missing file | |
24 | Given a required file | |
25 | But the file does not exist | |
26 | When I run cap "deploy:check:linked_files" | |
27 | Then the task will exit | |
28 | ||
29 | Scenario: Checking linked files - file exists | |
30 | Given a required file | |
31 | And that file exists | |
32 | When I run cap "deploy:check:linked_files" | |
33 | Then the task will be successful | |
34 | ||
35 | Scenario: Creating a release | |
36 | Given I run cap "deploy:check:directories" | |
37 | When I run cap "git:create_release" as part of a release | |
38 | Then the repo is cloned | |
39 | And the release is created | |
40 | ||
41 | Scenario: Symlink linked files | |
42 | When I run cap "deploy:symlink:linked_files" as part of a release | |
43 | Then file symlinks are created in the new release | |
44 | ||
45 | Scenario: Symlink linked dirs | |
46 | When I run cap "deploy:symlink:linked_dirs" as part of a release | |
47 | Then directory symlinks are created in the new release | |
48 | ||
49 | Scenario: Publishing | |
50 | When I run cap "deploy:symlink:release" | |
51 | Then the current directory will be a symlink to the release | |
52 |
0 | Feature: Deploy failure | |
1 | ||
2 | Background: | |
3 | Given a test app with the default configuration | |
4 | And a custom task that will simulate a failure | |
5 | And a custom task to run in the event of a failure | |
6 | And servers with the roles app and web | |
7 | ||
8 | Scenario: Triggering the custom task | |
9 | When I run cap "deploy:starting" | |
10 | But an error is raised | |
11 | Then the failure task will not run | |
12 | ||
13 | Scenario: Triggering the custom task | |
14 | When I run cap "deploy" | |
15 | But an error is raised | |
16 | Then the failure task will run |
0 | Feature: Installation | |
1 | ||
2 | Background: | |
3 | Given a test app with the default configuration | |
4 | ||
5 | Scenario: With default stages | |
6 | When I run cap "install" | |
7 | Then the deploy.rb file is created | |
8 | And the default stage files are created | |
9 | And the tasks folder is created | |
10 | ||
11 | Scenario: With specified stages | |
12 | When I run cap "install STAGES=qa,production" | |
13 | Then the deploy.rb file is created | |
14 | And the specified stage files are created | |
15 | And the tasks folder is created |
0 | Feature: Remote file task | |
1 | ||
2 | Background: | |
3 | Given a test app with the default configuration | |
4 | And a custom task to generate a file | |
5 | And servers with the roles app and web | |
6 | ||
7 | Scenario: Where the file does not exist | |
8 | When I run cap "deploy:check:linked_files" | |
9 | Then it creates the file with the remote_task prerequisite | |
10 | ||
11 | Scenario: Where the file already exists | |
12 | When I run cap "deploy:check:linked_files" | |
13 | Then it will not recreate the file |
0 | Then(/^references in the remote repo are listed$/) do | |
1 | end | |
2 | ||
3 | Then(/^the shared path is created$/) do | |
4 | run_vagrant_command(test_dir_exists(TestApp.shared_path)) | |
5 | end | |
6 | ||
7 | Then(/^the releases path is created$/) do | |
8 | run_vagrant_command(test_dir_exists(TestApp.releases_path)) | |
9 | end | |
10 | ||
11 | Then(/^directories in :linked_dirs are created in shared$/) do | |
12 | TestApp.linked_dirs.each do |dir| | |
13 | run_vagrant_command(test_dir_exists(TestApp.shared_path.join(dir))) | |
14 | end | |
15 | end | |
16 | ||
17 | Then(/^directories referenced in :linked_files are created in shared$/) do | |
18 | dirs = TestApp.linked_files.map { |path| TestApp.shared_path.join(path).dirname } | |
19 | dirs.each do | dir| | |
20 | run_vagrant_command(test_dir_exists(dir)) | |
21 | end | |
22 | end | |
23 | ||
24 | Then(/^the task will be successful$/) do | |
25 | end | |
26 | ||
27 | ||
28 | Then(/^the task will exit$/) do | |
29 | end | |
30 | ||
31 | Then(/^the repo is cloned$/) do | |
32 | run_vagrant_command(test_dir_exists(TestApp.repo_path)) | |
33 | end | |
34 | ||
35 | Then(/^the release is created$/) do | |
36 | run_vagrant_command("ls -g #{TestApp.releases_path}") | |
37 | end | |
38 | ||
39 | Then(/^file symlinks are created in the new release$/) do | |
40 | pending | |
41 | TestApp.linked_files.each do |file| | |
42 | run_vagrant_command(test_symlink_exists(TestApp.release_path.join(file))) | |
43 | end | |
44 | end | |
45 | ||
46 | Then(/^directory symlinks are created in the new release$/) do | |
47 | pending | |
48 | TestApp.linked_dirs.each do |dir| | |
49 | run_vagrant_command(test_symlink_exists(TestApp.release_path.join(dir))) | |
50 | end | |
51 | end | |
52 | ||
53 | Then(/^the current directory will be a symlink to the release$/) do | |
54 | run_vagrant_command(test_symlink_exists(TestApp.current_path)) | |
55 | end | |
56 | ||
57 | Then(/^the deploy\.rb file is created$/) do | |
58 | file = TestApp.test_app_path.join('config/deploy.rb') | |
59 | expect(File.exists?(file)).to be_true | |
60 | end | |
61 | ||
62 | Then(/^the default stage files are created$/) do | |
63 | staging = TestApp.test_app_path.join('config/deploy/staging.rb') | |
64 | production = TestApp.test_app_path.join('config/deploy/production.rb') | |
65 | expect(File.exists?(staging)).to be_true | |
66 | expect(File.exists?(production)).to be_true | |
67 | end | |
68 | ||
69 | Then(/^the tasks folder is created$/) do | |
70 | path = TestApp.test_app_path.join('lib/capistrano/tasks') | |
71 | expect(Dir.exists?(path)).to be_true | |
72 | end | |
73 | ||
74 | Then(/^the specified stage files are created$/) do | |
75 | qa = TestApp.test_app_path.join('config/deploy/qa.rb') | |
76 | production = TestApp.test_app_path.join('config/deploy/production.rb') | |
77 | expect(File.exists?(qa)).to be_true | |
78 | expect(File.exists?(production)).to be_true | |
79 | end | |
80 | ||
81 | Then(/^it creates the file with the remote_task prerequisite$/) do | |
82 | TestApp.linked_files.each do |file| | |
83 | run_vagrant_command(test_file_exists(TestApp.shared_path.join(file))) | |
84 | end | |
85 | end | |
86 | ||
87 | Then(/^it will not recreate the file$/) do | |
88 | # | |
89 | end | |
90 | ||
91 | Then(/^the task is successful$/) do | |
92 | expect(@success).to be_true | |
93 | end | |
94 | ||
95 | Then(/^the failure task will run$/) do | |
96 | failed = TestApp.shared_path.join('failed') | |
97 | run_vagrant_command(test_file_exists(failed)) | |
98 | end | |
99 | ||
100 | Then(/^the failure task will not run$/) do | |
101 | failed = TestApp.shared_path.join('failed') | |
102 | !run_vagrant_command(test_file_exists(failed)) | |
103 | end | |
104 | ||
105 | When(/^an error is raised$/) do | |
106 | error = TestApp.shared_path.join('fail') | |
107 | run_vagrant_command(test_file_exists(error)) | |
108 | end | |
109 | ||
110 | Then(/contains "(.*?)" in the output/) do |expected| | |
111 | expect(@output).to include(expected) | |
112 | end |
0 | When(/^I run cap "(.*?)"$/) do |task| | |
1 | @success = TestApp.cap(task) | |
2 | end | |
3 | ||
4 | When(/^I run cap "(.*?)" as part of a release$/) do |task| | |
5 | TestApp.cap("deploy:new_release_path #{task}") | |
6 | end | |
7 | ||
8 | When(/^I run "(.*?)"$/) do |command| | |
9 | @success, @output = TestApp.run(command) | |
10 | end | |
11 |
0 | Given(/^a test app with the default configuration$/) do | |
1 | TestApp.install | |
2 | end | |
3 | ||
4 | Given(/^servers with the roles app and web$/) do | |
5 | vagrant_cli_command('up') rescue nil | |
6 | end | |
7 | ||
8 | Given(/^a required file$/) do | |
9 | end | |
10 | ||
11 | Given(/^that file exists$/) do | |
12 | run_vagrant_command("touch #{TestApp.linked_file}") | |
13 | end | |
14 | ||
15 | Given(/^the file does not exist$/) do | |
16 | pending | |
17 | file = TestApp.linked_file | |
18 | run_vagrant_command("[ -f #{file} ] && rm #{file}") | |
19 | end | |
20 | ||
21 | Given(/^a custom task to generate a file$/) do | |
22 | TestApp.copy_task_to_test_app('spec/support/tasks/database.rake') | |
23 | end | |
24 | ||
25 | Given(/config stage file has line "(.*?)"/) do |line| | |
26 | TestApp.append_to_deploy_file(line) | |
27 | end | |
28 | ||
29 | Given(/^the configuration is in a custom location$/) do | |
30 | TestApp.move_configuration_to_custom_location('app') | |
31 | end | |
32 | ||
33 | Given(/^a custom task that will simulate a failure$/) do | |
34 | safely_remove_file(TestApp.shared_path.join('failed')) | |
35 | TestApp.copy_task_to_test_app('spec/support/tasks/fail.rake') | |
36 | end | |
37 | ||
38 | Given(/^a custom task to run in the event of a failure$/) do | |
39 | safely_remove_file(TestApp.shared_path.join('failed')) | |
40 | TestApp.copy_task_to_test_app('spec/support/tasks/failed.rake') | |
41 | end |
0 | require 'kuroko' | |
1 | ||
2 | project_root = File.expand_path('../../../', __FILE__) | |
3 | vagrant_root = File.join(project_root, 'spec/support') | |
4 | ||
5 | Kuroko.configure do |config| | |
6 | config.vagrant_root = 'spec/support' | |
7 | end | |
8 | ||
9 | puts vagrant_root.inspect | |
10 | ||
11 | require_relative '../../spec/support/test_app' |
0 | module RemoteCommandHelpers | |
1 | ||
2 | def test_dir_exists(path) | |
3 | exists?('d', path) | |
4 | end | |
5 | ||
6 | def test_symlink_exists(path) | |
7 | exists?('L', path) | |
8 | end | |
9 | ||
10 | def test_file_exists(path) | |
11 | exists?('f', path) | |
12 | end | |
13 | ||
14 | def exists?(type, path) | |
15 | %{[ -#{type} "#{path}" ] && echo "#{path} exists." || echo "Error: #{path} does not exist."} | |
16 | end | |
17 | ||
18 | def safely_remove_file(path) | |
19 | run_vagrant_command("rm #{test_file}") rescue Vagrant::Errors::VagrantError | |
20 | end | |
21 | end | |
22 | ||
23 | World(RemoteCommandHelpers) |
0 | require 'rake' | |
1 | require 'sshkit' | |
2 | require 'sshkit/dsl' | |
3 | ||
4 | Rake.application.options.trace = true | |
5 | ||
6 | require 'capistrano/version' | |
7 | require 'capistrano/version_validator' | |
8 | require 'capistrano/i18n' | |
9 | require 'capistrano/dsl' | |
10 | require 'capistrano/application' | |
11 | require 'capistrano/configuration' | |
12 | ||
13 | module Capistrano | |
14 | ||
15 | end |
0 | module Capistrano | |
1 | class Application < Rake::Application | |
2 | ||
3 | def initialize | |
4 | super | |
5 | @rakefiles = %w{capfile Capfile capfile.rb Capfile.rb} << capfile | |
6 | end | |
7 | ||
8 | def name | |
9 | "cap" | |
10 | end | |
11 | ||
12 | def run | |
13 | Rake.application = self | |
14 | super | |
15 | end | |
16 | ||
17 | def sort_options(options) | |
18 | options.push(version, roles, dry_run, hostfilter) | |
19 | super | |
20 | end | |
21 | ||
22 | def top_level_tasks | |
23 | if tasks_without_stage_dependency.include?(@top_level_tasks.first) | |
24 | @top_level_tasks | |
25 | else | |
26 | @top_level_tasks.unshift(ensure_stage.to_s) | |
27 | end | |
28 | end | |
29 | ||
30 | def exit_because_of_exception(ex) | |
31 | if respond_to?(:deploying?) && deploying? | |
32 | exit_deploy_because_of_exception(ex) | |
33 | else | |
34 | super | |
35 | end | |
36 | end | |
37 | ||
38 | private | |
39 | ||
40 | def load_imports | |
41 | if options.show_tasks | |
42 | invoke 'load:defaults' | |
43 | set(:stage, '') | |
44 | Dir[deploy_config_path, stage_definitions].each { |f| add_import f } | |
45 | end | |
46 | ||
47 | super | |
48 | end | |
49 | ||
50 | # allows the `cap install` task to load without a capfile | |
51 | def capfile | |
52 | File.expand_path(File.join(File.dirname(__FILE__),'..','Capfile')) | |
53 | end | |
54 | ||
55 | def version | |
56 | ['--version', '-V', | |
57 | "Display the program version.", | |
58 | lambda { |value| | |
59 | puts "Capistrano Version: #{Capistrano::VERSION} (Rake Version: #{RAKEVERSION})" | |
60 | exit | |
61 | } | |
62 | ] | |
63 | end | |
64 | ||
65 | def dry_run | |
66 | ['--dry-run', '-n', | |
67 | "Do a dry run without executing actions", | |
68 | lambda { |value| | |
69 | Configuration.env.set(:sshkit_backend, SSHKit::Backend::Printer) | |
70 | } | |
71 | ] | |
72 | end | |
73 | ||
74 | def roles | |
75 | ['--roles ROLES', '-r', | |
76 | "Filter command to only apply to these roles (separate multiple roles with a comma)", | |
77 | lambda { |value| | |
78 | Configuration.env.set(:filter, roles: value.split(",")) | |
79 | } | |
80 | ] | |
81 | end | |
82 | ||
83 | def hostfilter | |
84 | ['--hosts HOSTS', '-z', | |
85 | "Filter command to only apply to these hosts (separate multiple hosts with a comma)", | |
86 | lambda { |value| | |
87 | Configuration.env.set(:filter, hosts: value.split(",")) | |
88 | } | |
89 | ] | |
90 | end | |
91 | ||
92 | end | |
93 | ||
94 | end |
0 | module Capistrano | |
1 | class Callback | |
2 | attr_reader :source, :options, :only, :except | |
3 | ||
4 | def initialize(source, options={}) | |
5 | @source = source | |
6 | @options = options | |
7 | @only = Array(options[:only]).map { |v| v.to_s } | |
8 | @except = Array(options[:except]).map { |v| v.to_s } | |
9 | end | |
10 | ||
11 | def applies_to?(task) | |
12 | if task && only.any? | |
13 | return only.include?(task.fully_qualified_name) | |
14 | elsif task && except.any? | |
15 | return !except.include?(task.fully_qualified_name) | |
16 | else | |
17 | return true | |
18 | end | |
19 | end | |
20 | end | |
21 | ||
22 | class ProcCallback < Callback | |
23 | def call | |
24 | source.call | |
25 | end | |
26 | end | |
27 | ||
28 | class TaskCallback < Callback | |
29 | attr_reader :config | |
30 | ||
31 | def initialize(config, source, options={}) | |
32 | super(source, options) | |
33 | @config = config | |
34 | end | |
35 | ||
36 | def call | |
37 | config.find_and_execute_task(source) | |
38 | end | |
39 | ||
40 | def applies_to?(task) | |
41 | super && (task.nil? || task.fully_qualified_name != source.to_s) | |
42 | end | |
43 | end | |
44 | end⏎ |
0 | require 'capistrano/configuration' | |
1 | ||
2 | module Capistrano | |
3 | class CLI | |
4 | module Execute | |
5 | def self.included(base) #:nodoc: | |
6 | base.extend(ClassMethods) | |
7 | end | |
8 | ||
9 | module ClassMethods | |
10 | # Invoke capistrano using the ARGV array as the option parameters. This | |
11 | # is what the command-line capistrano utility does. | |
12 | def execute | |
13 | parse(ARGV).execute! | |
14 | end | |
15 | end | |
16 | ||
17 | # Using the options build when the command-line was parsed, instantiate | |
18 | # a new Capistrano configuration, initialize it, and execute the | |
19 | # requested actions. | |
20 | # | |
21 | # Returns the Configuration instance used, if successful. | |
22 | def execute! | |
23 | config = instantiate_configuration(options) | |
24 | config.debug = options[:debug] | |
25 | config.dry_run = options[:dry_run] | |
26 | config.preserve_roles = options[:preserve_roles] | |
27 | config.logger.level = options[:verbose] | |
28 | ||
29 | set_pre_vars(config) | |
30 | load_recipes(config) | |
31 | ||
32 | config.trigger(:load) | |
33 | execute_requested_actions(config) | |
34 | config.trigger(:exit) | |
35 | ||
36 | config | |
37 | rescue Exception => error | |
38 | handle_error(error) | |
39 | end | |
40 | ||
41 | def execute_requested_actions(config) | |
42 | Array(options[:vars]).each { |name, value| config.set(name, value) } | |
43 | ||
44 | Array(options[:actions]).each do |action| | |
45 | config.find_and_execute_task(action, :before => :start, :after => :finish) | |
46 | end | |
47 | end | |
48 | ||
49 | def set_pre_vars(config) #:nodoc: | |
50 | config.set :password, options[:password] | |
51 | Array(options[:pre_vars]).each { |name, value| config.set(name, value) } | |
52 | end | |
53 | ||
54 | def load_recipes(config) #:nodoc: | |
55 | # load the standard recipe definition | |
56 | config.load "standard" | |
57 | ||
58 | # load systemwide config/recipe definition | |
59 | config.load(options[:sysconf]) if options[:sysconf] && File.file?(options[:sysconf]) | |
60 | ||
61 | # load user config/recipe definition | |
62 | config.load(options[:dotfile]) if options[:dotfile] && File.file?(options[:dotfile]) | |
63 | ||
64 | Array(options[:recipes]).each { |recipe| config.load(recipe) } | |
65 | end | |
66 | ||
67 | # Primarily useful for testing, but subclasses of CLI could conceivably | |
68 | # override this method to return a Configuration subclass or replacement. | |
69 | def instantiate_configuration(options={}) #:nodoc: | |
70 | Capistrano::Configuration.new(options) | |
71 | end | |
72 | ||
73 | def handle_error(error) #:nodoc: | |
74 | case error | |
75 | when Net::SSH::AuthenticationFailed | |
76 | abort "authentication failed for `#{error.message}'" | |
77 | when Capistrano::Error | |
78 | abort(error.message) | |
79 | else raise error | |
80 | end | |
81 | end | |
82 | end | |
83 | end | |
84 | end |
0 | module Capistrano | |
1 | class CLI | |
2 | module Help | |
3 | LINE_PADDING = 7 | |
4 | MIN_MAX_LEN = 30 | |
5 | HEADER_LEN = 60 | |
6 | ||
7 | def self.included(base) #:nodoc: | |
8 | base.send :alias_method, :execute_requested_actions_without_help, :execute_requested_actions | |
9 | base.send :alias_method, :execute_requested_actions, :execute_requested_actions_with_help | |
10 | end | |
11 | ||
12 | def execute_requested_actions_with_help(config) | |
13 | if options[:tasks] | |
14 | task_list(config, options[:tasks]) | |
15 | elsif options[:explain] | |
16 | explain_task(config, options[:explain]) | |
17 | else | |
18 | execute_requested_actions_without_help(config) | |
19 | end | |
20 | end | |
21 | ||
22 | def task_list(config, pattern = true) #:nodoc: | |
23 | tool_output = options[:tool] | |
24 | ||
25 | if pattern.is_a?(String) | |
26 | tasks = config.task_list(:all).select {|t| t.fully_qualified_name =~ /#{pattern}/} | |
27 | end | |
28 | if tasks.nil? || tasks.length == 0 | |
29 | warn "Pattern '#{pattern}' not found. Listing all tasks.\n\n" if !tool_output && !pattern.is_a?(TrueClass) | |
30 | tasks = config.task_list(:all) | |
31 | end | |
32 | ||
33 | if tasks.empty? | |
34 | warn "There are no tasks available. Please specify a recipe file to load." unless tool_output | |
35 | else | |
36 | all_tasks_length = tasks.length | |
37 | if options[:verbose].to_i < 1 | |
38 | tasks = tasks.reject { |t| t.description.empty? || t.description =~ /^\[internal\]/ } | |
39 | end | |
40 | ||
41 | tasks = tasks.sort_by { |task| task.fully_qualified_name } | |
42 | ||
43 | longest = tasks.map { |task| task.fully_qualified_name.length }.max | |
44 | max_length = output_columns - longest - LINE_PADDING | |
45 | max_length = MIN_MAX_LEN if max_length < MIN_MAX_LEN | |
46 | ||
47 | tasks.each do |task| | |
48 | if tool_output | |
49 | puts "cap #{task.fully_qualified_name}" | |
50 | else | |
51 | puts "cap %-#{longest}s # %s" % [task.fully_qualified_name, task.brief_description(max_length)] | |
52 | end | |
53 | end | |
54 | ||
55 | unless tool_output | |
56 | if all_tasks_length > tasks.length | |
57 | puts | |
58 | puts "Some tasks were not listed, either because they have no description," | |
59 | puts "or because they are only used internally by other tasks. To see all" | |
60 | puts "tasks, type `#{File.basename($0)} -vT'." | |
61 | end | |
62 | ||
63 | puts | |
64 | puts "Extended help may be available for these tasks." | |
65 | puts "Type `#{File.basename($0)} -e taskname' to view it." | |
66 | end | |
67 | end | |
68 | end | |
69 | ||
70 | def explain_task(config, name) #:nodoc: | |
71 | task = config.find_task(name) | |
72 | if task.nil? | |
73 | warn "The task `#{name}' does not exist." | |
74 | else | |
75 | puts "-" * HEADER_LEN | |
76 | puts "cap #{name}" | |
77 | puts "-" * HEADER_LEN | |
78 | ||
79 | if task.description.empty? | |
80 | puts "There is no description for this task." | |
81 | else | |
82 | puts format_text(task.description) | |
83 | end | |
84 | ||
85 | puts | |
86 | end | |
87 | end | |
88 | ||
89 | def long_help #:nodoc: | |
90 | help_text = File.read(File.join(File.dirname(__FILE__), "help.txt")) | |
91 | self.class.ui.page_at = self.class.ui.output_rows - 2 | |
92 | self.class.ui.say format_text(help_text) | |
93 | end | |
94 | ||
95 | def format_text(text) #:nodoc: | |
96 | formatted = "" | |
97 | text.each_line do |line| | |
98 | indentation = line[/^\s+/] || "" | |
99 | indentation_size = indentation.split(//).inject(0) { |c,s| c + (s[0] == ?\t ? 8 : 1) } | |
100 | line_length = output_columns - indentation_size | |
101 | line_length = MIN_MAX_LEN if line_length < MIN_MAX_LEN | |
102 | lines = line.strip.gsub(/(.{1,#{line_length}})(?:\s+|\Z)/, "\\1\n").split(/\n/) | |
103 | if lines.empty? | |
104 | formatted << "\n" | |
105 | else | |
106 | formatted << lines.map { |l| "#{indentation}#{l}\n" }.join | |
107 | end | |
108 | end | |
109 | formatted | |
110 | end | |
111 | ||
112 | def output_columns #:nodoc: | |
113 | if ( @output_columns.nil? ) | |
114 | if ( self.class.ui.output_cols.nil? || self.class.ui.output_cols > 80 ) | |
115 | @output_columns = 80 | |
116 | else | |
117 | @output_columns = self.class.ui.output_cols | |
118 | end | |
119 | end | |
120 | @output_columns | |
121 | end | |
122 | end | |
123 | end | |
124 | end |
0 | ----------------------------- | |
1 | <%= color('Capistrano', :bold) %> | |
2 | ----------------------------- | |
3 | ||
4 | Capistrano is a utility for automating the execution of commands across multiple remote machines. It was originally conceived as an aid to deploy Ruby on Rails web applications, but has since evolved to become a much more general-purpose tool. | |
5 | ||
6 | The command-line interface to Capistrano is via the `cap' command. | |
7 | ||
8 | cap [ option ] ... action ... | |
9 | ||
10 | The following options are understood: | |
11 | ||
12 | <%= color '-d, --debug', :bold %> | |
13 | Causes Capistrano to pause and prompt before executing any remote command, giving the user the option to either execute the command, skip the command, or abort execution entirely. This makes it a great way to troubleshoot tasks, or test custom tasks, by executing commands one at a time and checking the server to make sure they worked as expected before moving on to the next command. (Compare this to the --dry-run command.) | |
14 | ||
15 | <%= color '-e, --explain TASK', :bold %> | |
16 | Displays the extended description of the given task. Not all tasks will have an extended description, but for those that do, this can provide a wealth of additional usage information, such as describing environment variables or settings that can affect the execution of the task. | |
17 | ||
18 | <%= color '-F, --default-config', :bold %> | |
19 | By default, cap will search for a config file named `Capfile' or `capfile' in the current directory, or in any parent directory, and will automatically load it. However, if you specify the -f flag (see below), cap will use that file instead of the default config. If you want to use both the default config, and files loaded via -f, you can specify -F to force cap to search for and load the default config, even if additional files were specified via -f. | |
20 | ||
21 | <%= color '-f, --file FILE', :bold %> | |
22 | Causes the named file to be loaded. Capistrano will search both its own recipe directory, as well as the current directory, looking for the named file. An ".rb" extension is optional. The -f option may be given any number of times, but if it is given, it will take the place of the normal `Capfile' or `capfile' detection. Use -F if you want the default capfile to be loaded when you use -f. | |
23 | ||
24 | <%= color '-H, --long-help', :bold %> | |
25 | Displays this document and exits. | |
26 | ||
27 | <%= color '-h, --help', :bold %> | |
28 | Shows a brief summary of these options and exits. | |
29 | ||
30 | <%= color '-l, --logger [STDERR|STDOUT|file]', :bold %> | |
31 | Change the file used to print the output. It offers three options: standard error(stderr), standard output and file. Options are not case sensitive. By default Capistrano uses stderr. | |
32 | ||
33 | <%= color '-n, --dry-run', :bold %> | |
34 | Causes Capistrano to simply display each remote command, without executing it. In this sense it is similar to --debug, but without the prompt. Note that commands executed locally are still run--only remote commands are skipped. | |
35 | ||
36 | <%= color '-p, --password', :bold %> | |
37 | Normally, cap will prompt for the password on-demand, the first time it is needed. This can make it hard to walk away from Capistrano, since you might not know if it will prompt for a password down the road. In such cases, you can use the -p option to force cap to prompt for the password immediately. | |
38 | ||
39 | <%= color '-q, --quiet', :bold %> | |
40 | Display only critical error messages. All other output is suppressed. | |
41 | ||
42 | <%= color '-S, --set-before NAME=VALUE', :bold %> | |
43 | Sets the given variable to the given value, before loading any recipe files. This is useful if you have a recipe file that depends on a certain variable being set, at the time it is loaded. | |
44 | ||
45 | <%= color '-s, --set NAME=VALUE', :bold %> | |
46 | Sets the given variable to the given value, after loading all recipe files. This is useful when you want to override the value of a variable which is used in a task. Note that this will set the variables too late for them to affect conditions that are executed as the recipes are loaded. | |
47 | ||
48 | <%= color '-T, --tasks PATTERN', :bold %> | |
49 | Displays the list of all tasks (matching optional PATTERN) in all loaded recipe files. If a task has no description, or if the description starts with the [internal] tag, the task will not be listed unless you also specify -v. | |
50 | ||
51 | <%= color '-t, --tool', :bold %> | |
52 | Abbreviates the output of -T for integration with other tools. Without -t, -T will list tasks with their summaries, and may include additional instructive text at the bottom. When integrating with other tools (e.g., bash auto-expansion and the like) that additional text can get in the way. This switch makes it easier for those tools to parse the list of tasks. (The -t switch has no effect if the -T switch is not specified.) | |
53 | ||
54 | <%= color '-V, --version', :bold %> | |
55 | Shows the current Capistrano version number and exits. | |
56 | ||
57 | <%= color '-v, --verbose', :bold %> | |
58 | Increase the verbosity. You can specify this option up to three times to further increase verbosity. By default, cap will use maximum verbosity, but if you specify an explicit verbosity, that will be used instead. See also -q. | |
59 | ||
60 | <%= color '-X, --skip-system-config', :bold %> | |
61 | By default, cap will look for and (if it exists) load the global system configuration file located in /etc/capistrano.conf. If you don't want cap to load that file, give this option. | |
62 | ||
63 | <%= color '-x, --skip-user-config', :bold %> | |
64 | By default, cap will look for and (if it exists) load the user-specific configuration file located in $HOME/.caprc. If you don't want cap to load that file, give this option. | |
65 | ||
66 | ----------------------------- | |
67 | <%= color('Environment Variables', :bold) %> | |
68 | ----------------------------- | |
69 | ||
70 | <%= color 'HOSTS', :bold %> | |
71 | Execute the tasks against this comma-separated list of hosts. Effectively, this makes the host(s) part of every roles. | |
72 | ||
73 | <%= color 'HOSTFILTER', :bold %> | |
74 | Execute tasks against this comma-separated list of host, but only if the host has the proper role for the task. | |
75 | ||
76 | <%= color 'HOSTROLEFILTER', :bold %> | |
77 | Execute tasks against the hosts in this comma-separated list of roles, but only if the host has the proper role for the task. | |
78 | ||
79 | <%= color 'ROLES', :bold %> | |
80 | Execute tasks against this comma-separated list of roles. Hosts which do not have the right roles will be skipped. |
0 | require 'optparse' | |
1 | ||
2 | module Capistrano | |
3 | class CLI | |
4 | module Options | |
5 | def self.included(base) | |
6 | base.extend(ClassMethods) | |
7 | end | |
8 | ||
9 | module ClassMethods | |
10 | # Return a new CLI instance with the given arguments pre-parsed and | |
11 | # ready for execution. | |
12 | def parse(args) | |
13 | cli = new(args) | |
14 | cli.parse_options! | |
15 | cli | |
16 | end | |
17 | end | |
18 | ||
19 | # The hash of (parsed) command-line options | |
20 | attr_reader :options | |
21 | ||
22 | # Return an OptionParser instance that defines the acceptable command | |
23 | # line switches for Capistrano, and what their corresponding behaviors | |
24 | # are. | |
25 | def option_parser #:nodoc: | |
26 | @logger = Logger.new | |
27 | @option_parser ||= OptionParser.new do |opts| | |
28 | opts.banner = "Usage: #{File.basename($0)} [options] action ..." | |
29 | ||
30 | opts.on("-d", "--debug", | |
31 | "Prompts before each remote command execution." | |
32 | ) { |value| options[:debug] = true } | |
33 | ||
34 | opts.on("-e", "--explain TASK", | |
35 | "Displays help (if available) for the task." | |
36 | ) { |value| options[:explain] = value } | |
37 | ||
38 | opts.on("-F", "--default-config", | |
39 | "Always use default config, even with -f." | |
40 | ) { options[:default_config] = true } | |
41 | ||
42 | opts.on("-f", "--file FILE", | |
43 | "A recipe file to load. May be given more than once." | |
44 | ) { |value| options[:recipes] << value } | |
45 | ||
46 | opts.on("-H", "--long-help", "Explain these options and environment variables.") do | |
47 | long_help | |
48 | exit | |
49 | end | |
50 | ||
51 | opts.on("-h", "--help", "Display this help message.") do | |
52 | puts opts | |
53 | exit | |
54 | end | |
55 | ||
56 | opts.on("-l", "--logger [STDERR|STDOUT|file]", | |
57 | "Choose logger method. STDERR used by default." | |
58 | ) do |value| | |
59 | options[:output] = if value.nil? || value.upcase == 'STDERR' | |
60 | # Using default logger. | |
61 | nil | |
62 | elsif value.upcase == 'STDOUT' | |
63 | $stdout | |
64 | else | |
65 | value | |
66 | end | |
67 | end | |
68 | ||
69 | opts.on("-n", "--dry-run", | |
70 | "Prints out commands without running them." | |
71 | ) { |value| options[:dry_run] = true } | |
72 | ||
73 | opts.on("-p", "--password", | |
74 | "Immediately prompt for the password." | |
75 | ) { options[:password] = nil } | |
76 | ||
77 | opts.on("-q", "--quiet", | |
78 | "Make the output as quiet as possible." | |
79 | ) { options[:verbose] = 0 } | |
80 | ||
81 | opts.on("-r", "--preserve-roles", | |
82 | "Preserve task roles" | |
83 | ) { options[:preserve_roles] = true } | |
84 | ||
85 | opts.on("-S", "--set-before NAME=VALUE", | |
86 | "Set a variable before the recipes are loaded." | |
87 | ) do |pair| | |
88 | name, value = pair.split(/=/, 2) | |
89 | options[:pre_vars][name.to_sym] = value | |
90 | end | |
91 | ||
92 | opts.on("-s", "--set NAME=VALUE", | |
93 | "Set a variable after the recipes are loaded." | |
94 | ) do |pair| | |
95 | name, value = pair.split(/=/, 2) | |
96 | options[:vars][name.to_sym] = value | |
97 | end | |
98 | ||
99 | opts.on("-T", "--tasks [PATTERN]", | |
100 | "List all tasks (matching optional PATTERN) in the loaded recipe files." | |
101 | ) do |value| | |
102 | options[:tasks] = if value | |
103 | value | |
104 | else | |
105 | true | |
106 | end | |
107 | options[:verbose] ||= 0 | |
108 | end | |
109 | ||
110 | opts.on("-t", "--tool", | |
111 | "Abbreviates the output of -T for tool integration." | |
112 | ) { options[:tool] = true } | |
113 | ||
114 | opts.on("-V", "--version", | |
115 | "Display the Capistrano version, and exit." | |
116 | ) do | |
117 | require 'capistrano/version' | |
118 | puts "Capistrano v#{Capistrano::Version}" | |
119 | exit | |
120 | end | |
121 | ||
122 | opts.on("-v", "--verbose", | |
123 | "Be more verbose. May be given more than once." | |
124 | ) do | |
125 | options[:verbose] ||= 0 | |
126 | options[:verbose] += 1 | |
127 | end | |
128 | ||
129 | opts.on("-X", "--skip-system-config", | |
130 | "Don't load the system config file (capistrano.conf)" | |
131 | ) { options.delete(:sysconf) } | |
132 | ||
133 | opts.on("-x", "--skip-user-config", | |
134 | "Don't load the user config file (.caprc)" | |
135 | ) { options.delete(:dotfile) } | |
136 | end | |
137 | end | |
138 | ||
139 | # If the arguments to the command are empty, this will print the | |
140 | # allowed options and exit. Otherwise, it will parse the command | |
141 | # line and set up any default options. | |
142 | def parse_options! #:nodoc: | |
143 | @options = { :recipes => [], :actions => [], | |
144 | :vars => {}, :pre_vars => {}, | |
145 | :sysconf => default_sysconf, :dotfile => default_dotfile } | |
146 | ||
147 | if args.empty? | |
148 | warn "Please specify at least one action to execute." | |
149 | warn option_parser | |
150 | exit | |
151 | end | |
152 | ||
153 | option_parser.parse!(args) | |
154 | ||
155 | coerce_variable_types! | |
156 | ||
157 | # if no verbosity has been specified, be verbose | |
158 | options[:verbose] = 3 if !options.has_key?(:verbose) | |
159 | ||
160 | look_for_default_recipe_file! if options[:default_config] || options[:recipes].empty? | |
161 | extract_environment_variables! | |
162 | ||
163 | options[:actions].concat(args) | |
164 | ||
165 | password = options.has_key?(:password) | |
166 | options[:password] = Proc.new { self.class.password_prompt } | |
167 | options[:password] = options[:password].call if password | |
168 | end | |
169 | ||
170 | # Extracts name=value pairs from the remaining command-line arguments | |
171 | # and assigns them as environment variables. | |
172 | def extract_environment_variables! #:nodoc: | |
173 | args.delete_if do |arg| | |
174 | next unless arg.match(/^(\w+)=(.*)$/) | |
175 | ENV[$1] = $2 | |
176 | end | |
177 | end | |
178 | ||
179 | # Looks for a default recipe file in the current directory. | |
180 | def look_for_default_recipe_file! #:nodoc: | |
181 | current = Dir.pwd | |
182 | ||
183 | loop do | |
184 | %w(Capfile capfile).each do |file| | |
185 | if File.file?(file) | |
186 | options[:recipes] << file | |
187 | @logger.info "Using recipes from #{File.join(current,file)}" | |
188 | return | |
189 | end | |
190 | end | |
191 | ||
192 | pwd = Dir.pwd | |
193 | Dir.chdir("..") | |
194 | break if pwd == Dir.pwd # if changing the directory made no difference, then we're at the top | |
195 | end | |
196 | ||
197 | Dir.chdir(current) | |
198 | end | |
199 | ||
200 | def default_sysconf #:nodoc: | |
201 | File.join(sysconf_directory, "capistrano.conf") | |
202 | end | |
203 | ||
204 | def default_dotfile #:nodoc: | |
205 | File.join(home_directory, ".caprc") | |
206 | end | |
207 | ||
208 | def sysconf_directory #:nodoc: | |
209 | # TODO if anyone cares, feel free to submit a patch that uses a more | |
210 | # appropriate location for this file in Windows. | |
211 | ENV["SystemRoot"] || '/etc' | |
212 | end | |
213 | ||
214 | def home_directory #:nodoc: | |
215 | ENV["HOME"] || | |
216 | (ENV["HOMEPATH"] && "#{ENV["HOMEDRIVE"]}#{ENV["HOMEPATH"]}") || | |
217 | "/" | |
218 | end | |
219 | ||
220 | def coerce_variable_types! | |
221 | [:pre_vars, :vars].each do |collection| | |
222 | options[collection].keys.each do |key| | |
223 | options[collection][key] = coerce_variable(options[collection][key]) | |
224 | end | |
225 | end | |
226 | end | |
227 | ||
228 | def coerce_variable(value) | |
229 | case value | |
230 | when /^"(.*)"$/ then $1 | |
231 | when /^'(.*)'$/ then $1 | |
232 | when /^\d+$/ then value.to_i | |
233 | when /^\d+\.\d*$/ then value.to_f | |
234 | when "true" then true | |
235 | when "false" then false | |
236 | when "nil" then nil | |
237 | else value | |
238 | end | |
239 | end | |
240 | end | |
241 | end | |
242 | end |
0 | require 'highline' | |
1 | ||
2 | # work around problem where HighLine detects an eof on $stdin and raises an | |
3 | # error. | |
4 | HighLine.track_eof = false | |
5 | ||
6 | module Capistrano | |
7 | class CLI | |
8 | module UI | |
9 | def self.included(base) #:nodoc: | |
10 | base.extend(ClassMethods) | |
11 | end | |
12 | ||
13 | module ClassMethods | |
14 | # Return the object that provides UI-specific methods, such as prompts | |
15 | # and more. | |
16 | def ui | |
17 | @ui ||= HighLine.new | |
18 | end | |
19 | ||
20 | # Prompt for a password using echo suppression. | |
21 | def password_prompt(prompt="Password: ") | |
22 | ui.ask(prompt) { |q| q.echo = false } | |
23 | end | |
24 | ||
25 | # Debug mode prompt | |
26 | def debug_prompt(cmd) | |
27 | ui.say("Preparing to execute command: #{cmd}") | |
28 | prompt = "Execute ([Yes], No, Abort) " | |
29 | ui.ask("#{prompt}? ") do |q| | |
30 | q.overwrite = false | |
31 | q.default = 'y' | |
32 | q.validate = /(y(es)?)|(no?)|(a(bort)?|\n)/i | |
33 | q.responses[:not_valid] = prompt | |
34 | end | |
35 | end | |
36 | end | |
37 | end | |
38 | end | |
39 | end |
0 | require 'capistrano' | |
1 | require 'capistrano/cli/execute' | |
2 | require 'capistrano/cli/help' | |
3 | require 'capistrano/cli/options' | |
4 | require 'capistrano/cli/ui' | |
5 | ||
6 | module Capistrano | |
7 | # The CLI class encapsulates the behavior of capistrano when it is invoked | |
8 | # as a command-line utility. This allows other programs to embed Capistrano | |
9 | # and preserve its command-line semantics. | |
10 | class CLI | |
11 | # The array of (unparsed) command-line options | |
12 | attr_reader :args | |
13 | ||
14 | # Create a new CLI instance using the given array of command-line parameters | |
15 | # to initialize it. By default, +ARGV+ is used, but you can specify a | |
16 | # different set of parameters (such as when embedded cap in a program): | |
17 | # | |
18 | # require 'capistrano/cli' | |
19 | # Capistrano::CLI.parse(%W(-vvvv -f config/deploy update_code)).execute! | |
20 | # | |
21 | # Note that you can also embed cap directly by creating a new Configuration | |
22 | # instance and setting it up, The above snippet, redone using the | |
23 | # Configuration class directly, would look like: | |
24 | # | |
25 | # require 'capistrano' | |
26 | # require 'capistrano/cli' | |
27 | # config = Capistrano::Configuration.new | |
28 | # config.logger.level = Capistrano::Logger::TRACE | |
29 | # config.set(:password) { Capistrano::CLI.password_prompt } | |
30 | # config.load "config/deploy" | |
31 | # config.update_code | |
32 | # | |
33 | # There may be times that you want/need the additional control offered by | |
34 | # manipulating the Configuration directly, but generally interfacing with | |
35 | # the CLI class is recommended. | |
36 | def initialize(args) | |
37 | @args = args.dup | |
38 | $stdout.sync = true # so that Net::SSH prompts show up | |
39 | end | |
40 | ||
41 | # Mix-in the actual behavior | |
42 | include Execute, Options, UI | |
43 | include Help # needs to be included last, because it overrides some methods | |
44 | ||
45 | end | |
46 | end |
0 | require 'benchmark' | |
1 | require 'capistrano/errors' | |
2 | require 'capistrano/processable' | |
3 | ||
4 | module Capistrano | |
5 | ||
6 | # This class encapsulates a single command to be executed on a set of remote | |
7 | # machines, in parallel. | |
8 | class Command | |
9 | include Processable | |
10 | ||
11 | class Tree | |
12 | attr_reader :configuration | |
13 | attr_reader :branches | |
14 | attr_reader :fallback | |
15 | ||
16 | include Enumerable | |
17 | ||
18 | class Branch | |
19 | attr_accessor :command, :callback | |
20 | attr_reader :options | |
21 | ||
22 | def initialize(command, options, callback) | |
23 | @command = command.strip.gsub(/\r?\n/, "\\\n") | |
24 | @callback = callback || Capistrano::Configuration.default_io_proc | |
25 | @options = options | |
26 | @skip = false | |
27 | end | |
28 | ||
29 | def last? | |
30 | options[:last] | |
31 | end | |
32 | ||
33 | def skip? | |
34 | @skip | |
35 | end | |
36 | ||
37 | def skip! | |
38 | @skip = true | |
39 | end | |
40 | ||
41 | def match(server) | |
42 | true | |
43 | end | |
44 | ||
45 | def to_s | |
46 | command.inspect | |
47 | end | |
48 | end | |
49 | ||
50 | class ConditionBranch < Branch | |
51 | attr_accessor :configuration | |
52 | attr_accessor :condition | |
53 | ||
54 | class Evaluator | |
55 | attr_reader :configuration, :condition, :server | |
56 | ||
57 | def initialize(config, condition, server) | |
58 | @configuration = config | |
59 | @condition = condition | |
60 | @server = server | |
61 | end | |
62 | ||
63 | def in?(role) | |
64 | configuration.roles[role].include?(server) | |
65 | end | |
66 | ||
67 | def result | |
68 | eval(condition, binding) | |
69 | end | |
70 | ||
71 | def method_missing(sym, *args, &block) | |
72 | if server.respond_to?(sym) | |
73 | server.send(sym, *args, &block) | |
74 | elsif configuration.respond_to?(sym) | |
75 | configuration.send(sym, *args, &block) | |
76 | else | |
77 | super | |
78 | end | |
79 | end | |
80 | end | |
81 | ||
82 | def initialize(configuration, condition, command, options, callback) | |
83 | @configuration = configuration | |
84 | @condition = condition | |
85 | super(command, options, callback) | |
86 | end | |
87 | ||
88 | def match(server) | |
89 | Evaluator.new(configuration, condition, server).result | |
90 | end | |
91 | ||
92 | def to_s | |
93 | "#{condition.inspect} :: #{command.inspect}" | |
94 | end | |
95 | end | |
96 | ||
97 | def initialize(config) | |
98 | @configuration = config | |
99 | @branches = [] | |
100 | yield self if block_given? | |
101 | end | |
102 | ||
103 | def when(condition, command, options={}, &block) | |
104 | branches << ConditionBranch.new(configuration, condition, command, options, block) | |
105 | end | |
106 | ||
107 | def else(command, &block) | |
108 | @fallback = Branch.new(command, {}, block) | |
109 | end | |
110 | ||
111 | def branches_for(server) | |
112 | seen_last = false | |
113 | matches = branches.select do |branch| | |
114 | success = !seen_last && !branch.skip? && branch.match(server) | |
115 | seen_last = success && branch.last? | |
116 | success | |
117 | end | |
118 | ||
119 | matches << fallback if matches.empty? && fallback | |
120 | return matches | |
121 | end | |
122 | ||
123 | def each | |
124 | branches.each { |branch| yield branch } | |
125 | yield fallback if fallback | |
126 | return self | |
127 | end | |
128 | end | |
129 | ||
130 | attr_reader :tree, :sessions, :options | |
131 | ||
132 | def self.process(tree, sessions, options={}) | |
133 | new(tree, sessions, options).process! | |
134 | end | |
135 | ||
136 | # Instantiates a new command object. The +command+ must be a string | |
137 | # containing the command to execute. +sessions+ is an array of Net::SSH | |
138 | # session instances, and +options+ must be a hash containing any of the | |
139 | # following keys: | |
140 | # | |
141 | # * +logger+: (optional), a Capistrano::Logger instance | |
142 | # * +data+: (optional), a string to be sent to the command via it's stdin | |
143 | # * +env+: (optional), a string or hash to be interpreted as environment | |
144 | # variables that should be defined for this command invocation. | |
145 | def initialize(tree, sessions, options={}, &block) | |
146 | if String === tree | |
147 | tree = Tree.new(nil) { |t| t.else(tree, &block) } | |
148 | elsif block | |
149 | raise ArgumentError, "block given with tree argument" | |
150 | end | |
151 | ||
152 | @tree = tree | |
153 | @sessions = sessions | |
154 | @options = options | |
155 | @channels = open_channels | |
156 | end | |
157 | ||
158 | # Processes the command in parallel on all specified hosts. If the command | |
159 | # fails (non-zero return code) on any of the hosts, this will raise a | |
160 | # Capistrano::CommandError. | |
161 | def process! | |
162 | elapsed = Benchmark.realtime do | |
163 | loop do | |
164 | break unless process_iteration { @channels.any? { |ch| !ch[:closed] } } | |
165 | end | |
166 | end | |
167 | ||
168 | logger.trace "command finished in #{(elapsed * 1000).round}ms" if logger | |
169 | ||
170 | if (failed = @channels.select { |ch| ch[:status] != 0 }).any? | |
171 | commands = failed.inject({}) { |map, ch| (map[ch[:command]] ||= []) << ch[:server]; map } | |
172 | message = commands.map { |command, list| "#{command.inspect} on #{list.join(',')}" }.join("; ") | |
173 | error = CommandError.new("failed: #{message}") | |
174 | error.hosts = commands.values.flatten | |
175 | raise error | |
176 | end | |
177 | ||
178 | self | |
179 | end | |
180 | ||
181 | # Force the command to stop processing, by closing all open channels | |
182 | # associated with this command. | |
183 | def stop! | |
184 | @channels.each do |ch| | |
185 | ch.close unless ch[:closed] | |
186 | end | |
187 | end | |
188 | ||
189 | private | |
190 | ||
191 | def logger | |
192 | options[:logger] | |
193 | end | |
194 | ||
195 | def open_channels | |
196 | sessions.map do |session| | |
197 | server = session.xserver | |
198 | tree.branches_for(server).map do |branch| | |
199 | session.open_channel do |channel| | |
200 | channel[:server] = server | |
201 | channel[:host] = server.host | |
202 | channel[:options] = options | |
203 | channel[:branch] = branch | |
204 | ||
205 | request_pty_if_necessary(channel) do |ch, success| | |
206 | if success | |
207 | logger.trace "executing command", ch[:server] if logger | |
208 | cmd = replace_placeholders(channel[:branch].command, ch) | |
209 | ||
210 | if options[:shell] == false | |
211 | shell = nil | |
212 | else | |
213 | shell = "#{options[:shell] || "sh"} -c" | |
214 | cmd = cmd.gsub(/'/) { |m| "'\\''" } | |
215 | cmd = "'#{cmd}'" | |
216 | end | |
217 | ||
218 | command_line = [environment, shell, cmd].compact.join(" ") | |
219 | ch[:command] = command_line | |
220 | ||
221 | ch.exec(command_line) | |
222 | ch.send_data(options[:data]) if options[:data] | |
223 | else | |
224 | # just log it, don't actually raise an exception, since the | |
225 | # process method will see that the status is not zero and will | |
226 | # raise an exception then. | |
227 | logger.important "could not open channel", ch[:server] if logger | |
228 | ch.close | |
229 | end | |
230 | end | |
231 | ||
232 | channel.on_data do |ch, data| | |
233 | ch[:branch].callback[ch, :out, data] | |
234 | end | |
235 | ||
236 | channel.on_extended_data do |ch, type, data| | |
237 | ch[:branch].callback[ch, :err, data] | |
238 | end | |
239 | ||
240 | channel.on_request("exit-status") do |ch, data| | |
241 | ch[:status] = data.read_long | |
242 | end | |
243 | ||
244 | channel.on_close do |ch| | |
245 | ch[:closed] = true | |
246 | end | |
247 | end | |
248 | end | |
249 | end.flatten | |
250 | end | |
251 | ||
252 | def request_pty_if_necessary(channel) | |
253 | if options[:pty] | |
254 | channel.request_pty do |ch, success| | |
255 | yield ch, success | |
256 | end | |
257 | else | |
258 | yield channel, true | |
259 | end | |
260 | end | |
261 | ||
262 | def replace_placeholders(command, channel) | |
263 | roles = @tree.configuration && @tree.configuration.role_names_for_host(channel[:server]) | |
264 | command = command.gsub(/\$CAPISTRANO:HOST\$/, channel[:host]) | |
265 | command.gsub!(/\$CAPISTRANO:HOSTROLES\$/, roles.join(',')) if roles | |
266 | command | |
267 | end | |
268 | ||
269 | # prepare a space-separated sequence of variables assignments | |
270 | # intended to be prepended to a command, so the shell sets | |
271 | # the environment before running the command. | |
272 | # i.e.: options[:env] = {'PATH' => '/opt/ruby/bin:$PATH', | |
273 | # 'TEST' => '( "quoted" )'} | |
274 | # environment returns: | |
275 | # "env TEST=(\ \"quoted\"\ ) PATH=/opt/ruby/bin:$PATH" | |
276 | def environment | |
277 | return if options[:env].nil? || options[:env].empty? | |
278 | @environment ||= if String === options[:env] | |
279 | "env #{options[:env]}" | |
280 | else | |
281 | options[:env].inject("env") do |string, (name, value)| | |
282 | value = value.to_s.gsub(/[ "]/) { |m| "\\#{m}" } | |
283 | string << " #{name}=#{value}" | |
284 | end | |
285 | end | |
286 | end | |
287 | end | |
288 | end |
0 | require 'capistrano/transfer' | |
1 | ||
2 | module Capistrano | |
3 | class Configuration | |
4 | module Actions | |
5 | module FileTransfer | |
6 | ||
7 | # Store the given data at the given location on all servers targetted | |
8 | # by the current task. If <tt>:mode</tt> is specified it is used to | |
9 | # set the mode on the file. | |
10 | def put(data, path, options={}) | |
11 | opts = options.dup | |
12 | upload(StringIO.new(data), path, opts) | |
13 | end | |
14 | ||
15 | # Get file remote_path from FIRST server targeted by | |
16 | # the current task and transfer it to local machine as path. | |
17 | # | |
18 | # get "#{deploy_to}/current/log/production.log", "log/production.log.web" | |
19 | def get(remote_path, path, options={}, &block) | |
20 | download(remote_path, path, options.merge(:once => true), &block) | |
21 | end | |
22 | ||
23 | def upload(from, to, options={}, &block) | |
24 | mode = options.delete(:mode) | |
25 | transfer(:up, from, to, options, &block) | |
26 | if mode | |
27 | mode = mode.is_a?(Numeric) ? mode.to_s(8) : mode.to_s | |
28 | run "chmod #{mode} #{to}", options | |
29 | end | |
30 | end | |
31 | ||
32 | def download(from, to, options={}, &block) | |
33 | transfer(:down, from, to, options, &block) | |
34 | end | |
35 | ||
36 | def transfer(direction, from, to, options={}, &block) | |
37 | if dry_run | |
38 | return logger.debug "transfering: #{[direction, from, to] * ', '}" | |
39 | end | |
40 | execute_on_servers(options) do |servers| | |
41 | targets = servers.map { |s| sessions[s] } | |
42 | Transfer.process(direction, from, to, targets, options.merge(:logger => logger), &block) | |
43 | end | |
44 | end | |
45 | ||
46 | end | |
47 | end | |
48 | end | |
49 | end |
0 | require 'capistrano/errors' | |
1 | ||
2 | module Capistrano | |
3 | class Configuration | |
4 | module Actions | |
5 | module Inspect | |
6 | ||
7 | # Streams the result of the command from all servers that are the | |
8 | # target of the current task. All these streams will be joined into a | |
9 | # single one, so you can, say, watch 10 log files as though they were | |
10 | # one. Do note that this is quite expensive from a bandwidth | |
11 | # perspective, so use it with care. | |
12 | # | |
13 | # The command is invoked via #invoke_command. | |
14 | # | |
15 | # Usage: | |
16 | # | |
17 | # desc "Run a tail on multiple log files at the same time" | |
18 | # task :tail_fcgi, :roles => :app do | |
19 | # stream "tail -f #{shared_path}/log/fastcgi.crash.log" | |
20 | # end | |
21 | def stream(command, options={}) | |
22 | invoke_command(command, options) do |ch, stream, out| | |
23 | puts out if stream == :out | |
24 | warn "[err :: #{ch[:server]}] #{out}" if stream == :err | |
25 | end | |
26 | end | |
27 | ||
28 | # Executes the given command on the first server targetted by the | |
29 | # current task, collects it's stdout into a string, and returns the | |
30 | # string. The command is invoked via #invoke_command. | |
31 | def capture(command, options={}) | |
32 | output = "" | |
33 | invoke_command(command, options.merge(:once => true)) do |ch, stream, data| | |
34 | case stream | |
35 | when :out then output << data | |
36 | when :err then warn "[err :: #{ch[:server]}] #{data}" | |
37 | end | |
38 | end | |
39 | output | |
40 | end | |
41 | ||
42 | end | |
43 | end | |
44 | end | |
45 | end |
0 | require 'capistrano/command' | |
1 | ||
2 | module Capistrano | |
3 | class Configuration | |
4 | module Actions | |
5 | module Invocation | |
6 | def self.included(base) #:nodoc: | |
7 | base.extend(ClassMethods) | |
8 | ||
9 | base.send :alias_method, :initialize_without_invocation, :initialize | |
10 | base.send :alias_method, :initialize, :initialize_with_invocation | |
11 | ||
12 | base.default_io_proc = Proc.new do |ch, stream, out| | |
13 | level = stream == :err ? :important : :info | |
14 | ch[:options][:logger].send(level, out, "#{stream} :: #{ch[:server]}") | |
15 | end | |
16 | end | |
17 | ||
18 | module ClassMethods | |
19 | attr_accessor :default_io_proc | |
20 | end | |
21 | ||
22 | def initialize_with_invocation(*args) #:nodoc: | |
23 | initialize_without_invocation(*args) | |
24 | set :default_environment, {} | |
25 | set :default_run_options, {} | |
26 | end | |
27 | ||
28 | # Executes different commands in parallel. This is useful for commands | |
29 | # that need to be different on different hosts, but which could be | |
30 | # otherwise run in parallel. | |
31 | # | |
32 | # The +options+ parameter is currently unused. | |
33 | # | |
34 | # Example: | |
35 | # | |
36 | # task :restart_everything do | |
37 | # parallel do |session| | |
38 | # session.when "in?(:app)", "/path/to/restart/mongrel" | |
39 | # session.when "in?(:web)", "/path/to/restart/apache" | |
40 | # session.when "in?(:db)", "/path/to/restart/mysql" | |
41 | # end | |
42 | # end | |
43 | # | |
44 | # Each command may have its own callback block, for capturing and | |
45 | # responding to output, with semantics identical to #run: | |
46 | # | |
47 | # session.when "in?(:app)", "/path/to/restart/mongrel" do |ch, stream, data| | |
48 | # # ch is the SSH channel for this command, used to send data | |
49 | # # back to the command (e.g. ch.send_data("password\n")) | |
50 | # # stream is either :out or :err, for which stream the data arrived on | |
51 | # # data is a string containing data sent from the remote command | |
52 | # end | |
53 | # | |
54 | # Also, you can specify a fallback command, to use when none of the | |
55 | # conditions match a server: | |
56 | # | |
57 | # session.else "/execute/something/else" | |
58 | # | |
59 | # The string specified as the first argument to +when+ may be any valid | |
60 | # Ruby code. It has access to the following variables and methods: | |
61 | # | |
62 | # * +in?(role)+ returns true if the server participates in the given role | |
63 | # * +server+ is the ServerDefinition object for the server. This can be | |
64 | # used to get the host-name, etc. | |
65 | # * +configuration+ is the current Capistrano::Configuration object, which | |
66 | # you can use to get the value of variables, etc. | |
67 | # | |
68 | # For example: | |
69 | # | |
70 | # session.when "server.host =~ /app/", "/some/command" | |
71 | # session.when "server.host == configuration[:some_var]", "/another/command" | |
72 | # session.when "in?(:web) || in?(:app)", "/more/commands" | |
73 | # | |
74 | # See #run for a description of the valid +options+. | |
75 | def parallel(options={}) | |
76 | raise ArgumentError, "parallel() requires a block" unless block_given? | |
77 | tree = Command::Tree.new(self) { |t| yield t } | |
78 | run_tree(tree, options) | |
79 | end | |
80 | ||
81 | # Invokes the given command. If a +via+ key is given, it will be used | |
82 | # to determine what method to use to invoke the command. It defaults | |
83 | # to :run, but may be :sudo, or any other method that conforms to the | |
84 | # same interface as run and sudo. | |
85 | def invoke_command(cmd, options={}, &block) | |
86 | options = options.dup | |
87 | via = options.delete(:via) || :run | |
88 | send(via, cmd, options, &block) | |
89 | end | |
90 | ||
91 | # Execute the given command on all servers that are the target of the | |
92 | # current task. If a block is given, it is invoked for all output | |
93 | # generated by the command, and should accept three parameters: the SSH | |
94 | # channel (which may be used to send data back to the remote process), | |
95 | # the stream identifier (<tt>:err</tt> for stderr, and <tt>:out</tt> for | |
96 | # stdout), and the data that was received. | |
97 | # | |
98 | # The +options+ hash may include any of the following keys: | |
99 | # | |
100 | # * :hosts - this is either a string (for a single target host) or an array | |
101 | # of strings, indicating which hosts the command should run on. By default, | |
102 | # the hosts are determined from the task definition. | |
103 | # * :roles - this is either a string or symbol (for a single target role) or | |
104 | # an array of strings or symbols, indicating which roles the command should | |
105 | # run on. If :hosts is specified, :roles will be ignored. | |
106 | # * :only - specifies a condition limiting which hosts will be selected to | |
107 | # run the command. This should refer to values set in the role definition. | |
108 | # For example, if a role is defined with :primary => true, then you could | |
109 | # select only hosts with :primary true by setting :only => { :primary => true }. | |
110 | # * :except - specifies a condition limiting which hosts will be selected to | |
111 | # run the command. This is the inverse of :only (hosts that do _not_ match | |
112 | # the condition will be selected). | |
113 | # * :on_no_matching_servers - if :continue, will continue to execute tasks if | |
114 | # no matching servers are found for the host criteria. The default is to raise | |
115 | # a NoMatchingServersError exception. | |
116 | # * :once - if true, only the first matching server will be selected. The default | |
117 | # is false (all matching servers will be selected). | |
118 | # * :max_hosts - specifies the maximum number of hosts that should be selected | |
119 | # at a time. If this value is less than the number of hosts that are selected | |
120 | # to run, then the hosts will be run in groups of max_hosts. The default is nil, | |
121 | # which indicates that there is no maximum host limit. Please note this does not | |
122 | # limit the number of SSH channels that can be open, only the number of hosts upon | |
123 | # which this will be called. | |
124 | # * :shell - says which shell should be used to invoke commands. This | |
125 | # defaults to "sh". Setting this to false causes Capistrano to invoke | |
126 | # the commands directly, without wrapping them in a shell invocation. | |
127 | # * :data - if not nil (the default), this should be a string that will | |
128 | # be passed to the command's stdin stream. | |
129 | # * :pty - if true, a pseudo-tty will be allocated for each command. The | |
130 | # default is false. Note that there are benefits and drawbacks both ways. | |
131 | # Empirically, it appears that if a pty is allocated, the SSH server daemon | |
132 | # will _not_ read user shell start-up scripts (e.g. bashrc, etc.). However, | |
133 | # if a pty is _not_ allocated, some commands will refuse to run in | |
134 | # interactive mode and will not prompt for (e.g.) passwords. | |
135 | # * :env - a hash of environment variable mappings that should be made | |
136 | # available to the command. The keys should be environment variable names, | |
137 | # and the values should be their corresponding values. The default is | |
138 | # empty, but may be modified by changing the +default_environment+ | |
139 | # Capistrano variable. | |
140 | # | |
141 | # Note that if you set these keys in the +default_run_options+ Capistrano | |
142 | # variable, they will apply for all invocations of #run, #invoke_command, | |
143 | # and #parallel. | |
144 | def run(cmd, options={}, &block) | |
145 | block ||= self.class.default_io_proc | |
146 | tree = Command::Tree.new(self) { |t| t.else(cmd, &block) } | |
147 | run_tree(tree, options) | |
148 | end | |
149 | ||
150 | # Executes a Capistrano::Command::Tree object. This is not for direct | |
151 | # use, but should instead be called indirectly, via #run or #parallel, | |
152 | # or #invoke_command. | |
153 | def run_tree(tree, options={}) #:nodoc: | |
154 | if tree.branches.empty? && tree.fallback | |
155 | logger.debug "executing #{tree.fallback}" | |
156 | elsif tree.branches.any? | |
157 | logger.debug "executing multiple commands in parallel" | |
158 | tree.each do |branch| | |
159 | logger.trace "-> #{branch}" | |
160 | end | |
161 | else | |
162 | raise ArgumentError, "attempt to execute without specifying a command" | |
163 | end | |
164 | ||
165 | return if dry_run || (debug && continue_execution(tree) == false) | |
166 | ||
167 | options = add_default_command_options(options) | |
168 | ||
169 | tree.each do |branch| | |
170 | if branch.command.include?(sudo) | |
171 | branch.callback = sudo_behavior_callback(branch.callback) | |
172 | end | |
173 | end | |
174 | ||
175 | execute_on_servers(options) do |servers| | |
176 | targets = servers.map { |s| sessions[s] } | |
177 | Command.process(tree, targets, options.merge(:logger => logger)) | |
178 | end | |
179 | end | |
180 | ||
181 | # Returns the command string used by capistrano to invoke a comamnd via | |
182 | # sudo. | |
183 | # | |
184 | # run "#{sudo :as => 'bob'} mkdir /path/to/dir" | |
185 | # | |
186 | # It can also be invoked like #run, but executing the command via sudo. | |
187 | # This assumes that the sudo password (if required) is the same as the | |
188 | # password for logging in to the server. | |
189 | # | |
190 | # sudo "mkdir /path/to/dir" | |
191 | # | |
192 | # Also, this method understands a <tt>:sudo</tt> configuration variable, | |
193 | # which (if specified) will be used as the full path to the sudo | |
194 | # executable on the remote machine: | |
195 | # | |
196 | # set :sudo, "/opt/local/bin/sudo" | |
197 | # | |
198 | # If you know what you're doing, you can also set <tt>:sudo_prompt</tt>, | |
199 | # which tells capistrano which prompt sudo should use when asking for | |
200 | # a password. (This is so that capistrano knows what prompt to look for | |
201 | # in the output.) If you set :sudo_prompt to an empty string, Capistrano | |
202 | # will not send a preferred prompt. | |
203 | def sudo(*parameters, &block) | |
204 | options = parameters.last.is_a?(Hash) ? parameters.pop.dup : {} | |
205 | command = parameters.first | |
206 | user = options[:as] && "-u #{options.delete(:as)}" | |
207 | ||
208 | sudo_prompt_option = "-p '#{sudo_prompt}'" unless sudo_prompt.empty? | |
209 | sudo_command = [fetch(:sudo, "sudo"), sudo_prompt_option, user].compact.join(" ") | |
210 | ||
211 | if command | |
212 | command = sudo_command + " " + command | |
213 | run(command, options, &block) | |
214 | else | |
215 | return sudo_command | |
216 | end | |
217 | end | |
218 | ||
219 | # Returns a Proc object that defines the behavior of the sudo | |
220 | # callback. The returned Proc will defer to the +fallback+ argument | |
221 | # (which should also be a Proc) for any output it does not | |
222 | # explicitly handle. | |
223 | def sudo_behavior_callback(fallback) #:nodoc: | |
224 | # in order to prevent _each host_ from prompting when the password | |
225 | # was wrong, let's track which host prompted first and only allow | |
226 | # subsequent prompts from that host. | |
227 | prompt_host = nil | |
228 | ||
229 | Proc.new do |ch, stream, out| | |
230 | if out =~ /^Sorry, try again/ | |
231 | if prompt_host.nil? || prompt_host == ch[:server] | |
232 | prompt_host = ch[:server] | |
233 | logger.important out, "#{stream} :: #{ch[:server]}" | |
234 | reset! :password | |
235 | end | |
236 | end | |
237 | ||
238 | if out =~ /^#{Regexp.escape(sudo_prompt)}/ | |
239 | ch.send_data "#{self[:password]}\n" | |
240 | elsif fallback | |
241 | fallback.call(ch, stream, out) | |
242 | end | |
243 | end | |
244 | end | |
245 | ||
246 | # Merges the various default command options into the options hash and | |
247 | # returns the result. The default command options that are understand | |
248 | # are: | |
249 | # | |
250 | # * :default_environment: If the :env key already exists, the :env | |
251 | # key is merged into default_environment and then added back into | |
252 | # options. | |
253 | # * :default_shell: if the :shell key already exists, it will be used. | |
254 | # Otherwise, if the :default_shell key exists in the configuration, | |
255 | # it will be used. Otherwise, no :shell key is added. | |
256 | def add_default_command_options(options) | |
257 | defaults = self[:default_run_options] | |
258 | options = defaults.merge(options) | |
259 | ||
260 | env = self[:default_environment] | |
261 | env = env.merge(options[:env]) if options[:env] | |
262 | options[:env] = env unless env.empty? | |
263 | ||
264 | shell = options[:shell] || self[:default_shell] | |
265 | options[:shell] = shell unless shell.nil? | |
266 | ||
267 | options | |
268 | end | |
269 | ||
270 | # Returns the prompt text to use with sudo | |
271 | def sudo_prompt | |
272 | fetch(:sudo_prompt, "sudo password: ") | |
273 | end | |
274 | ||
275 | def continue_execution(tree) | |
276 | if tree.branches.length == 1 | |
277 | continue_execution_for_branch(tree.branches.first) | |
278 | else | |
279 | tree.each { |branch| branch.skip! unless continue_execution_for_branch(branch) } | |
280 | tree.any? { |branch| !branch.skip? } | |
281 | end | |
282 | end | |
283 | ||
284 | def continue_execution_for_branch(branch) | |
285 | case Capistrano::CLI.debug_prompt(branch) | |
286 | when "y" | |
287 | true | |
288 | when "n" | |
289 | false | |
290 | when "a" | |
291 | exit(-1) | |
292 | end | |
293 | end | |
294 | end | |
295 | end | |
296 | end | |
297 | end |
0 | module Capistrano | |
1 | class Configuration | |
2 | module AliasTask | |
3 | # Attempts to find the task at the given fully-qualified path, and | |
4 | # alias it. If arguments don't have correct task names, an ArgumentError | |
5 | # wil be raised. If no such task exists, a Capistrano::NoSuchTaskError | |
6 | # will be raised. | |
7 | # | |
8 | # Usage: | |
9 | # | |
10 | # alias_task :original_deploy, :deploy | |
11 | # | |
12 | def alias_task(new_name, old_name) | |
13 | if !new_name.respond_to?(:to_sym) or !old_name.respond_to?(:to_sym) | |
14 | raise ArgumentError, "expected a valid task name" | |
15 | end | |
16 | ||
17 | original_task = find_task(old_name) or raise NoSuchTaskError, "the task `#{old_name}' does not exist" | |
18 | task = original_task.dup # Dup. task to avoid modify original task | |
19 | task.name = new_name | |
20 | ||
21 | define_task(task) | |
22 | end | |
23 | end | |
24 | end | |
25 | end |
0 | require 'capistrano/callback' | |
1 | ||
2 | module Capistrano | |
3 | class Configuration | |
4 | module Callbacks | |
5 | def self.included(base) #:nodoc: | |
6 | %w(initialize invoke_task_directly).each do |method| | |
7 | base.send :alias_method, "#{method}_without_callbacks", method | |
8 | base.send :alias_method, method, "#{method}_with_callbacks" | |
9 | end | |
10 | end | |
11 | ||
12 | # The hash of callbacks that have been registered for this configuration | |
13 | attr_reader :callbacks | |
14 | ||
15 | def initialize_with_callbacks(*args) #:nodoc: | |
16 | initialize_without_callbacks(*args) | |
17 | @callbacks = {} | |
18 | end | |
19 | ||
20 | def invoke_task_directly_with_callbacks(task) #:nodoc: | |
21 | ||
22 | trigger :before, task | |
23 | ||
24 | result = invoke_task_directly_without_callbacks(task) | |
25 | ||
26 | trigger :after, task | |
27 | ||
28 | return result | |
29 | end | |
30 | ||
31 | # Defines a callback to be invoked before the given task. You must | |
32 | # specify the fully-qualified task name, both for the primary task, and | |
33 | # for the task(s) to be executed before. Alternatively, you can pass a | |
34 | # block to be executed before the given task. | |
35 | # | |
36 | # before "deploy:update_code", :record_difference | |
37 | # before :deploy, "custom:log_deploy" | |
38 | # before :deploy, :this, "then:this", "and:then:this" | |
39 | # before :some_task do | |
40 | # puts "an anonymous hook!" | |
41 | # end | |
42 | # | |
43 | # This just provides a convenient interface to the more general #on method. | |
44 | def before(task_name, *args, &block) | |
45 | options = args.last.is_a?(Hash) ? args.pop : {} | |
46 | args << options.merge(:only => task_name) | |
47 | on :before, *args, &block | |
48 | end | |
49 | ||
50 | # Defines a callback to be invoked after the given task. You must | |
51 | # specify the fully-qualified task name, both for the primary task, and | |
52 | # for the task(s) to be executed after. Alternatively, you can pass a | |
53 | # block to be executed after the given task. | |
54 | # | |
55 | # after "deploy:update_code", :log_difference | |
56 | # after :deploy, "custom:announce" | |
57 | # after :deploy, :this, "then:this", "and:then:this" | |
58 | # after :some_task do | |
59 | # puts "an anonymous hook!" | |
60 | # end | |
61 | # | |
62 | # This just provides a convenient interface to the more general #on method. | |
63 | def after(task_name, *args, &block) | |
64 | options = args.last.is_a?(Hash) ? args.pop : {} | |
65 | args << options.merge(:only => task_name) | |
66 | on :after, *args, &block | |
67 | end | |
68 | ||
69 | # Defines one or more callbacks to be invoked in response to some event. | |
70 | # Capistrano currently understands the following events: | |
71 | # | |
72 | # * :before, triggered before a task is invoked | |
73 | # * :after, triggered after a task is invoked | |
74 | # * :start, triggered before a top-level task is invoked via the command-line | |
75 | # * :finish, triggered when a top-level task completes | |
76 | # * :load, triggered after all recipes have loaded | |
77 | # * :exit, triggered after all tasks have completed | |
78 | # | |
79 | # Specify the (fully-qualified) task names that you want invoked in | |
80 | # response to the event. Alternatively, you can specify a block to invoke | |
81 | # when the event is triggered. You can also pass a hash of options as the | |
82 | # last parameter, which may include either of two keys: | |
83 | # | |
84 | # * :only, should specify an array of task names. Restricts this callback | |
85 | # so that it will only fire when the event applies to those tasks. | |
86 | # * :except, should specify an array of task names. Restricts this callback | |
87 | # so that it will never fire when the event applies to those tasks. | |
88 | # | |
89 | # Usage: | |
90 | # | |
91 | # on :before, "some:hook", "another:hook", :only => "deploy:update" | |
92 | # on :after, "some:hook", :except => "deploy:create_symlink" | |
93 | # on :before, "global:hook" | |
94 | # on :after, :only => :deploy do | |
95 | # puts "after deploy here" | |
96 | # end | |
97 | def on(event, *args, &block) | |
98 | options = args.last.is_a?(Hash) ? args.pop : {} | |
99 | callbacks[event] ||= [] | |
100 | ||
101 | if args.empty? && block.nil? | |
102 | raise ArgumentError, "please specify either a task name or a block to invoke" | |
103 | elsif args.any? && block | |
104 | raise ArgumentError, "please specify only a task name or a block, but not both" | |
105 | elsif block | |
106 | callbacks[event] << ProcCallback.new(block, options) | |
107 | else | |
108 | args.each do |name| | |
109 | callbacks[event] << TaskCallback.new(self, name, options) | |
110 | end | |
111 | end | |
112 | end | |
113 | ||
114 | # Trigger the named event for the named task. All associated callbacks | |
115 | # will be fired, in the order they were defined. | |
116 | def trigger(event, task=nil) | |
117 | pending = Array(callbacks[event]).select { |c| c.applies_to?(task) } | |
118 | if pending.any? | |
119 | msg = "triggering #{event} callbacks" | |
120 | msg << " for `#{task.fully_qualified_name}'" if task | |
121 | logger.trace(msg) | |
122 | pending.each { |callback| callback.call } | |
123 | end | |
124 | end | |
125 | ||
126 | end | |
127 | end | |
128 | end |
0 | require 'enumerator' | |
1 | require 'net/ssh/gateway' | |
2 | require 'capistrano/ssh' | |
3 | require 'capistrano/errors' | |
4 | ||
5 | module Capistrano | |
6 | class Configuration | |
7 | module Connections | |
8 | def self.included(base) #:nodoc: | |
9 | base.send :alias_method, :initialize_without_connections, :initialize | |
10 | base.send :alias_method, :initialize, :initialize_with_connections | |
11 | end | |
12 | ||
13 | class DefaultConnectionFactory #:nodoc: | |
14 | def initialize(options) | |
15 | @options = options | |
16 | end | |
17 | ||
18 | def connect_to(server) | |
19 | SSH.connect(server, @options) | |
20 | end | |
21 | end | |
22 | ||
23 | class GatewayConnectionFactory #:nodoc: | |
24 | def initialize(gateway, options) | |
25 | @options = options | |
26 | Thread.abort_on_exception = true | |
27 | @gateways = {} | |
28 | if gateway.is_a?(Hash) | |
29 | @options[:logger].debug "Creating multiple gateways using #{gateway.inspect}" if @options[:logger] | |
30 | gateway.each do |gw, hosts| | |
31 | gateway_connection = add_gateway(gw) | |
32 | [*hosts].each do |host| | |
33 | @gateways[:default] ||= gateway_connection | |
34 | @gateways[host] = gateway_connection | |
35 | end | |
36 | end | |
37 | else | |
38 | @options[:logger].debug "Creating gateway using #{[*gateway].join(', ')}" if @options[:logger] | |
39 | @gateways[:default] = add_gateway(gateway) | |
40 | end | |
41 | end | |
42 | ||
43 | def add_gateway(gateway) | |
44 | gateways = [*gateway].collect { |g| ServerDefinition.new(g) } | |
45 | tunnel = SSH.connection_strategy(gateways[0], @options) do |host, user, connect_options| | |
46 | Net::SSH::Gateway.new(host, user, connect_options) | |
47 | end | |
48 | (gateways[1..-1]).inject(tunnel) do |tunnel, destination| | |
49 | @options[:logger].debug "Creating tunnel to #{destination}" if @options[:logger] | |
50 | local_host = ServerDefinition.new("127.0.0.1", :user => destination.user, :port => tunnel.open(destination.host, (destination.port || 22))) | |
51 | SSH.connection_strategy(local_host, @options) do |host, user, connect_options| | |
52 | Net::SSH::Gateway.new(host, user, connect_options) | |
53 | end | |
54 | end | |
55 | end | |
56 | ||
57 | def connect_to(server) | |
58 | @options[:logger].debug "establishing connection to `#{server}' via gateway" if @options[:logger] | |
59 | local_host = ServerDefinition.new("127.0.0.1", :user => server.user, :port => gateway_for(server).open(server.host, server.port || 22)) | |
60 | session = SSH.connect(local_host, @options) | |
61 | session.xserver = server | |
62 | session | |
63 | end | |
64 | ||
65 | def gateway_for(server) | |
66 | @gateways[server.host] || @gateways[:default] | |
67 | end | |
68 | end | |
69 | ||
70 | # A hash of the SSH sessions that are currently open and available. | |
71 | # Because sessions are constructed lazily, this will only contain | |
72 | # connections to those servers that have been the targets of one or more | |
73 | # executed tasks. Stored on a per-thread basis to improve thread-safety. | |
74 | def sessions | |
75 | Thread.current[:sessions] ||= {} | |
76 | end | |
77 | ||
78 | def initialize_with_connections(*args) #:nodoc: | |
79 | initialize_without_connections(*args) | |
80 | Thread.current[:sessions] = {} | |
81 | Thread.current[:failed_sessions] = [] | |
82 | end | |
83 | ||
84 | # Indicate that the given server could not be connected to. | |
85 | def failed!(server) | |
86 | Thread.current[:failed_sessions] << server | |
87 | end | |
88 | ||
89 | # Query whether previous connection attempts to the given server have | |
90 | # failed. | |
91 | def has_failed?(server) | |
92 | Thread.current[:failed_sessions].include?(server) | |
93 | end | |
94 | ||
95 | # Used to force connections to be made to the current task's servers. | |
96 | # Connections are normally made lazily in Capistrano--you can use this | |
97 | # to force them open before performing some operation that might be | |
98 | # time-sensitive. | |
99 | def connect!(options={}) | |
100 | execute_on_servers(options) { } | |
101 | end | |
102 | ||
103 | # Returns the object responsible for establishing new SSH connections. | |
104 | # The factory will respond to #connect_to, which can be used to | |
105 | # establish connections to servers defined via ServerDefinition objects. | |
106 | def connection_factory | |
107 | @connection_factory ||= begin | |
108 | if exists?(:gateway) | |
109 | logger.debug "establishing connection to gateway `#{fetch(:gateway).inspect}'" | |
110 | GatewayConnectionFactory.new(fetch(:gateway), self) | |
111 | else | |
112 | DefaultConnectionFactory.new(self) | |
113 | end | |
114 | end | |
115 | end | |
116 | ||
117 | # Ensures that there are active sessions for each server in the list. | |
118 | def establish_connections_to(servers) | |
119 | failed_servers = [] | |
120 | ||
121 | # force the connection factory to be instantiated synchronously, | |
122 | # otherwise we wind up with multiple gateway instances, because | |
123 | # each connection is done in parallel. | |
124 | connection_factory | |
125 | ||
126 | threads = Array(servers).map { |server| establish_connection_to(server, failed_servers) } | |
127 | threads.each { |t| t.join } | |
128 | ||
129 | if failed_servers.any? | |
130 | errors = failed_servers.map { |h| "#{h[:server]} (#{h[:error].class}: #{h[:error].message})" } | |
131 | error = ConnectionError.new("connection failed for: #{errors.join(', ')}") | |
132 | error.hosts = failed_servers.map { |h| h[:server] } | |
133 | raise error | |
134 | end | |
135 | end | |
136 | ||
137 | # Destroys sessions for each server in the list. | |
138 | def teardown_connections_to(servers) | |
139 | servers.each do |server| | |
140 | begin | |
141 | sessions.delete(server).close | |
142 | rescue IOError | |
143 | # the TCP connection is already dead | |
144 | end | |
145 | end | |
146 | end | |
147 | ||
148 | # Determines the set of servers within the current task's scope and | |
149 | # establishes connections to them, and then yields that list of | |
150 | # servers. | |
151 | def execute_on_servers(options={}) | |
152 | raise ArgumentError, "expected a block" unless block_given? | |
153 | ||
154 | if task = current_task | |
155 | servers = find_servers_for_task(task, options) | |
156 | ||
157 | if servers.empty? | |
158 | if ENV['HOSTFILTER'] || task.options.merge(options)[:on_no_matching_servers] == :continue | |
159 | logger.info "skipping `#{task.fully_qualified_name}' because no servers matched" | |
160 | return | |
161 | else | |
162 | raise Capistrano::NoMatchingServersError, "`#{task.fully_qualified_name}' is only run for servers matching #{task.options.inspect}, but no servers matched" | |
163 | end | |
164 | end | |
165 | ||
166 | if task.continue_on_error? | |
167 | servers.delete_if { |s| has_failed?(s) } | |
168 | return if servers.empty? | |
169 | end | |
170 | else | |
171 | servers = find_servers(options) | |
172 | if servers.empty? | |
173 | raise Capistrano::NoMatchingServersError, "no servers found to match #{options.inspect}" if options[:on_no_matching_servers] != :continue | |
174 | return | |
175 | end | |
176 | end | |
177 | ||
178 | servers = [servers.first] if options[:once] | |
179 | logger.trace "servers: #{servers.map { |s| s.host }.inspect}" | |
180 | ||
181 | max_hosts = (options[:max_hosts] || (task && task.max_hosts) || servers.size).to_i | |
182 | is_subset = max_hosts < servers.size | |
183 | ||
184 | # establish connections to those servers in groups of max_hosts, as necessary | |
185 | servers.each_slice(max_hosts) do |servers_slice| | |
186 | begin | |
187 | establish_connections_to(servers_slice) | |
188 | rescue ConnectionError => error | |
189 | raise error unless task && task.continue_on_error? | |
190 | error.hosts.each do |h| | |
191 | servers_slice.delete(h) | |
192 | failed!(h) | |
193 | end | |
194 | end | |
195 | ||
196 | begin | |
197 | yield servers_slice | |
198 | rescue RemoteError => error | |
199 | raise error unless task && task.continue_on_error? | |
200 | error.hosts.each { |h| failed!(h) } | |
201 | end | |
202 | ||
203 | # if dealing with a subset (e.g., :max_hosts is less than the | |
204 | # number of servers available) teardown the subset of connections | |
205 | # that were just made, so that we can make room for the next subset. | |
206 | teardown_connections_to(servers_slice) if is_subset | |
207 | end | |
208 | end | |
209 | ||
210 | private | |
211 | ||
212 | # We establish the connection by creating a thread in a new method--this | |
213 | # prevents problems with the thread's scope seeing the wrong 'server' | |
214 | # variable if the thread just happens to take too long to start up. | |
215 | def establish_connection_to(server, failures=nil) | |
216 | current_thread = Thread.current | |
217 | Thread.new { safely_establish_connection_to(server, current_thread, failures) } | |
218 | end | |
219 | ||
220 | def safely_establish_connection_to(server, thread, failures=nil) | |
221 | thread[:sessions] ||= {} | |
222 | thread[:sessions][server] ||= connection_factory.connect_to(server) | |
223 | rescue Exception => err | |
224 | raise unless failures | |
225 | failures << { :server => server, :error => err } | |
226 | end | |
227 | end | |
228 | end | |
229 | end |
0 | require 'capistrano/errors' | |
1 | ||
2 | module Capistrano | |
3 | class Configuration | |
4 | module Execution | |
5 | def self.included(base) #:nodoc: | |
6 | base.send :alias_method, :initialize_without_execution, :initialize | |
7 | base.send :alias_method, :initialize, :initialize_with_execution | |
8 | end | |
9 | ||
10 | # A struct for representing a single instance of an invoked task. | |
11 | TaskCallFrame = Struct.new(:task, :rollback) | |
12 | ||
13 | def initialize_with_execution(*args) #:nodoc: | |
14 | initialize_without_execution(*args) | |
15 | end | |
16 | private :initialize_with_execution | |
17 | ||
18 | # Returns true if there is a transaction currently active. | |
19 | def transaction? | |
20 | !rollback_requests.nil? | |
21 | end | |
22 | ||
23 | # The call stack of the tasks. The currently executing task may inspect | |
24 | # this to see who its caller was. The current task is always the last | |
25 | # element of this stack. | |
26 | def task_call_frames | |
27 | Thread.current[:task_call_frames] ||= [] | |
28 | end | |
29 | ||
30 | ||
31 | # The stack of tasks that have registered rollback handlers within the | |
32 | # current transaction. If this is nil, then there is no transaction | |
33 | # that is currently active. | |
34 | def rollback_requests | |
35 | Thread.current[:rollback_requests] | |
36 | end | |
37 | ||
38 | def rollback_requests=(rollback_requests) | |
39 | Thread.current[:rollback_requests] = rollback_requests | |
40 | end | |
41 | ||
42 | # Invoke a set of tasks in a transaction. If any task fails (raises an | |
43 | # exception), all tasks executed within the transaction are inspected to | |
44 | # see if they have an associated on_rollback hook, and if so, that hook | |
45 | # is called. | |
46 | def transaction | |
47 | raise ArgumentError, "expected a block" unless block_given? | |
48 | raise ScriptError, "transaction must be called from within a task" if task_call_frames.empty? | |
49 | ||
50 | return yield if transaction? | |
51 | ||
52 | logger.info "transaction: start" | |
53 | begin | |
54 | self.rollback_requests = [] | |
55 | yield | |
56 | logger.info "transaction: commit" | |
57 | rescue Object => e | |
58 | rollback! | |
59 | raise | |
60 | ensure | |
61 | self.rollback_requests = nil if Thread.main == Thread.current | |
62 | end | |
63 | end | |
64 | ||
65 | # Specifies an on_rollback hook for the currently executing task. If this | |
66 | # or any subsequent task then fails, and a transaction is active, this | |
67 | # hook will be executed. | |
68 | def on_rollback(&block) | |
69 | if transaction? | |
70 | # don't note a new rollback request if one has already been set | |
71 | rollback_requests << task_call_frames.last unless task_call_frames.last.rollback | |
72 | task_call_frames.last.rollback = block | |
73 | end | |
74 | end | |
75 | ||
76 | # Returns the TaskDefinition object for the currently executing task. | |
77 | # It returns nil if there is no task being executed. | |
78 | def current_task | |
79 | return nil if task_call_frames.empty? | |
80 | task_call_frames.last.task | |
81 | end | |
82 | ||
83 | # Executes the task with the given name, without invoking any associated | |
84 | # callbacks. | |
85 | def execute_task(task) | |
86 | logger.debug "executing `#{task.fully_qualified_name}'" | |
87 | push_task_call_frame(task) | |
88 | invoke_task_directly(task) | |
89 | ensure | |
90 | pop_task_call_frame | |
91 | end | |
92 | ||
93 | # Attempts to locate the task at the given fully-qualified path, and | |
94 | # execute it. If no such task exists, a Capistrano::NoSuchTaskError will | |
95 | # be raised. | |
96 | def find_and_execute_task(path, hooks={}) | |
97 | task = find_task(path) or raise NoSuchTaskError, "the task `#{path}' does not exist" | |
98 | ||
99 | trigger(hooks[:before], task) if hooks[:before] | |
100 | result = execute_task(task) | |
101 | trigger(hooks[:after], task) if hooks[:after] | |
102 | ||
103 | result | |
104 | end | |
105 | ||
106 | protected | |
107 | ||
108 | def rollback! | |
109 | return if Thread.current[:rollback_requests].nil? | |
110 | Thread.current[:rolled_back] = true | |
111 | ||
112 | # throw the task back on the stack so that roles are properly | |
113 | # interpreted in the scope of the task in question. | |
114 | rollback_requests.reverse.each do |frame| | |
115 | begin | |
116 | push_task_call_frame(frame.task) | |
117 | logger.important "rolling back", frame.task.fully_qualified_name | |
118 | frame.rollback.call | |
119 | rescue Object => e | |
120 | logger.info "exception while rolling back: #{e.class}, #{e.message}", frame.task.fully_qualified_name | |
121 | ensure | |
122 | pop_task_call_frame | |
123 | end | |
124 | end | |
125 | end | |
126 | ||
127 | def push_task_call_frame(task) | |
128 | frame = TaskCallFrame.new(task) | |
129 | task_call_frames.push frame | |
130 | end | |
131 | ||
132 | def pop_task_call_frame | |
133 | task_call_frames.pop | |
134 | end | |
135 | ||
136 | # Invokes the task's body directly, without setting up the call frame. | |
137 | def invoke_task_directly(task) | |
138 | task.namespace.instance_eval(&task.body) | |
139 | end | |
140 | end | |
141 | end | |
142 | end |
0 | module Capistrano | |
1 | class Configuration | |
2 | module Loading | |
3 | def self.included(base) #:nodoc: | |
4 | base.send :alias_method, :initialize_without_loading, :initialize | |
5 | base.send :alias_method, :initialize, :initialize_with_loading | |
6 | base.extend ClassMethods | |
7 | end | |
8 | ||
9 | module ClassMethods | |
10 | # Used by third-party task bundles to identify the capistrano | |
11 | # configuration that is loading them. Its return value is not reliable | |
12 | # in other contexts. If +require_config+ is not false, an exception | |
13 | # will be raised if the current configuration is not set. | |
14 | def instance(require_config=false) | |
15 | config = Thread.current[:capistrano_configuration] | |
16 | if require_config && config.nil? | |
17 | raise LoadError, "Please require this file from within a Capistrano recipe" | |
18 | end | |
19 | config | |
20 | end | |
21 | ||
22 | # Used internally by Capistrano to specify the current configuration | |
23 | # before loading a third-party task bundle. | |
24 | def instance=(config) | |
25 | Thread.current[:capistrano_configuration] = config | |
26 | end | |
27 | ||
28 | # Used internally by Capistrano to track which recipes have been loaded | |
29 | # via require, so that they may be successfully reloaded when require | |
30 | # is called again. | |
31 | def recipes_per_feature | |
32 | @recipes_per_feature ||= {} | |
33 | end | |
34 | ||
35 | # Used internally to determine what the current "feature" being | |
36 | # required is. This is used to track which files load which recipes | |
37 | # via require. | |
38 | def current_feature | |
39 | Thread.current[:capistrano_current_feature] | |
40 | end | |
41 | ||
42 | # Used internally to specify the current file being required, so that | |
43 | # any recipes loaded by that file can be remembered. This allows | |
44 | # recipes loaded via require to be correctly reloaded in different | |
45 | # Configuration instances in the same Ruby instance. | |
46 | def current_feature=(feature) | |
47 | Thread.current[:capistrano_current_feature] = feature | |
48 | end | |
49 | end | |
50 | ||
51 | # The load paths used for locating recipe files. | |
52 | attr_reader :load_paths | |
53 | ||
54 | def initialize_with_loading(*args) #:nodoc: | |
55 | initialize_without_loading(*args) | |
56 | @load_paths = [".", File.expand_path(File.join(File.dirname(__FILE__), "../recipes"))] | |
57 | @loaded_features = [] | |
58 | end | |
59 | private :initialize_with_loading | |
60 | ||
61 | # Load a configuration file or string into this configuration. | |
62 | # | |
63 | # Usage: | |
64 | # | |
65 | # load("recipe"): | |
66 | # Look for and load the contents of 'recipe.rb' into this | |
67 | # configuration. | |
68 | # | |
69 | # load(:file => "recipe"): | |
70 | # same as above | |
71 | # | |
72 | # load(:string => "set :scm, :subversion"): | |
73 | # Load the given string as a configuration specification. | |
74 | # | |
75 | # load { ... } | |
76 | # Load the block in the context of the configuration. | |
77 | def load(*args, &block) | |
78 | options = args.last.is_a?(Hash) ? args.pop : {} | |
79 | ||
80 | if block | |
81 | raise ArgumentError, "loading a block requires 0 arguments" unless options.empty? && args.empty? | |
82 | load(:proc => block) | |
83 | ||
84 | elsif args.any? | |
85 | args.each { |arg| load options.merge(:file => arg) } | |
86 | ||
87 | elsif options[:file] | |
88 | load_from_file(options[:file], options[:name]) | |
89 | ||
90 | elsif options[:string] | |
91 | remember_load(options) unless options[:reloading] | |
92 | instance_eval(options[:string], options[:name] || "<eval>") | |
93 | ||
94 | elsif options[:proc] | |
95 | remember_load(options) unless options[:reloading] | |
96 | instance_eval(&options[:proc]) | |
97 | ||
98 | else | |
99 | raise ArgumentError, "don't know how to load #{options.inspect}" | |
100 | end | |
101 | end | |
102 | ||
103 | # Require another file. This is identical to the standard require method, | |
104 | # with the exception that it sets the receiver as the "current" configuration | |
105 | # so that third-party task bundles can include themselves relative to | |
106 | # that configuration. | |
107 | # | |
108 | # This is a bit more complicated than an initial review would seem to | |
109 | # necessitate, but the use case that complicates things is this: An | |
110 | # advanced user wants to embed capistrano, and needs to instantiate | |
111 | # more than one capistrano configuration at a time. They also want each | |
112 | # configuration to require a third-party capistrano extension. Using a | |
113 | # naive require implementation, this would allow the first configuration | |
114 | # to successfully load the third-party extension, but the require would | |
115 | # fail for the second configuration because the extension has already | |
116 | # been loaded. | |
117 | # | |
118 | # To work around this, we do a few things: | |
119 | # | |
120 | # 1. Each time a 'require' is invoked inside of a capistrano recipe, | |
121 | # we remember the arguments (see "current_feature"). | |
122 | # 2. Each time a 'load' is invoked inside of a capistrano recipe, and | |
123 | # "current_feature" is not nil (meaning we are inside of a pending | |
124 | # require) we remember the options (see "remember_load" and | |
125 | # "recipes_per_feature"). | |
126 | # 3. Each time a 'require' is invoked inside of a capistrano recipe, | |
127 | # we check to see if this particular configuration has ever seen these | |
128 | # arguments to require (see @loaded_features), and if not, we proceed | |
129 | # as if the file had never been required. If the superclass' require | |
130 | # returns false (meaning, potentially, that the file has already been | |
131 | # required), then we look in the recipes_per_feature collection and | |
132 | # load any remembered recipes from there. | |
133 | # | |
134 | # It's kind of a bear, but it works, and works transparently. Note that | |
135 | # a simpler implementation would just muck with $", allowing files to be | |
136 | # required multiple times, but that will cause warnings (and possibly | |
137 | # errors) if the file to be required contains constant definitions and | |
138 | # such, alongside (or instead of) capistrano recipe definitions. | |
139 | def require(*args) #:nodoc: | |
140 | # look to see if this specific configuration instance has ever seen | |
141 | # these arguments to require before | |
142 | if @loaded_features.include?(args) | |
143 | return false | |
144 | end | |
145 | ||
146 | @loaded_features << args | |
147 | begin | |
148 | original_instance, self.class.instance = self.class.instance, self | |
149 | original_feature, self.class.current_feature = self.class.current_feature, args | |
150 | ||
151 | result = super | |
152 | if !result # file has been required previously, load up the remembered recipes | |
153 | list = self.class.recipes_per_feature[args] || [] | |
154 | list.each { |options| load(options.merge(:reloading => true)) } | |
155 | end | |
156 | ||
157 | return result | |
158 | ensure | |
159 | # restore the original, so that require's can be nested | |
160 | self.class.instance = original_instance | |
161 | self.class.current_feature = original_feature | |
162 | end | |
163 | end | |
164 | ||
165 | private | |
166 | ||
167 | # Load a recipe from the named file. If +name+ is given, the file will | |
168 | # be reported using that name. | |
169 | def load_from_file(file, name=nil) | |
170 | file = find_file_in_load_path(file) unless File.file?(file) | |
171 | load :string => File.read(file), :name => name || file | |
172 | end | |
173 | ||
174 | def find_file_in_load_path(file) | |
175 | load_paths.each do |path| | |
176 | ["", ".rb"].each do |ext| | |
177 | name = File.join(path, "#{file}#{ext}") | |
178 | return name if File.file?(name) | |
179 | end | |
180 | end | |
181 | ||
182 | raise LoadError, "no such file to load -- #{file}" | |
183 | end | |
184 | ||
185 | # If a file is being required, the options associated with loading a | |
186 | # recipe are remembered in the recipes_per_feature archive under the | |
187 | # name of the file currently being required. | |
188 | def remember_load(options) | |
189 | if self.class.current_feature | |
190 | list = (self.class.recipes_per_feature[self.class.current_feature] ||= []) | |
191 | list << options | |
192 | end | |
193 | end | |
194 | end | |
195 | end | |
196 | end |
0 | require 'capistrano/task_definition' | |
1 | ||
2 | module Capistrano | |
3 | class Configuration | |
4 | module Namespaces | |
5 | DEFAULT_TASK = :default | |
6 | ||
7 | def self.included(base) #:nodoc: | |
8 | base.send :alias_method, :initialize_without_namespaces, :initialize | |
9 | base.send :alias_method, :initialize, :initialize_with_namespaces | |
10 | end | |
11 | ||
12 | # The name of this namespace. Defaults to +nil+ for the top-level | |
13 | # namespace. | |
14 | attr_reader :name | |
15 | ||
16 | # The parent namespace of this namespace. Returns +nil+ for the top-level | |
17 | # namespace. | |
18 | attr_reader :parent | |
19 | ||
20 | # The hash of tasks defined for this namespace. | |
21 | attr_reader :tasks | |
22 | ||
23 | # The hash of namespaces defined for this namespace. | |
24 | attr_reader :namespaces | |
25 | ||
26 | def initialize_with_namespaces(*args) #:nodoc: | |
27 | @name = @parent = nil | |
28 | initialize_without_namespaces(*args) | |
29 | @tasks = {} | |
30 | @namespaces = {} | |
31 | end | |
32 | private :initialize_with_namespaces | |
33 | ||
34 | # Returns the top-level namespace (the one with no parent). | |
35 | def top | |
36 | return parent.top if parent | |
37 | return self | |
38 | end | |
39 | ||
40 | # Returns the fully-qualified name of this namespace, or nil if the | |
41 | # namespace is at the top-level. | |
42 | def fully_qualified_name | |
43 | return nil if name.nil? | |
44 | [parent.fully_qualified_name, name].compact.join(":") | |
45 | end | |
46 | ||
47 | # Describe the next task to be defined. The given text will be attached to | |
48 | # the next task that is defined and used as its description. | |
49 | def desc(text) | |
50 | @next_description = text | |
51 | end | |
52 | ||
53 | # Returns the value set by the last, pending "desc" call. If +reset+ is | |
54 | # not false, the value will be reset immediately afterwards. | |
55 | def next_description(reset=false) | |
56 | @next_description | |
57 | ensure | |
58 | @next_description = nil if reset | |
59 | end | |
60 | ||
61 | # Open a namespace in which to define new tasks. If the namespace was | |
62 | # defined previously, it will be reopened, otherwise a new namespace | |
63 | # will be created for the given name. | |
64 | def namespace(name, &block) | |
65 | name = name.to_sym | |
66 | raise ArgumentError, "expected a block" unless block_given? | |
67 | ||
68 | namespace_already_defined = namespaces.key?(name) | |
69 | if all_methods.any? { |m| m.to_sym == name } && !namespace_already_defined | |
70 | thing = tasks.key?(name) ? "task" : "method" | |
71 | raise ArgumentError, "defining a namespace named `#{name}' would shadow an existing #{thing} with that name" | |
72 | end | |
73 | ||
74 | namespaces[name] ||= Namespace.new(name, self) | |
75 | namespaces[name].instance_eval(&block) | |
76 | ||
77 | # make sure any open description gets terminated | |
78 | namespaces[name].desc(nil) | |
79 | ||
80 | if !namespace_already_defined | |
81 | metaclass = class << self; self; end | |
82 | metaclass.send(:define_method, name) { namespaces[name] } | |
83 | end | |
84 | end | |
85 | ||
86 | # Describe a new task. If a description is active (see #desc), it is added | |
87 | # to the options under the <tt>:desc</tt> key. The new task is added to | |
88 | # the namespace. | |
89 | def task(name, options={}, &block) | |
90 | name = name.to_sym | |
91 | raise ArgumentError, "expected a block" unless block_given? | |
92 | ||
93 | task_already_defined = tasks.key?(name) | |
94 | if all_methods.any? { |m| m.to_sym == name } && !task_already_defined | |
95 | thing = namespaces.key?(name) ? "namespace" : "method" | |
96 | raise ArgumentError, "defining a task named `#{name}' would shadow an existing #{thing} with that name" | |
97 | end | |
98 | ||
99 | ||
100 | task = TaskDefinition.new(name, self, {:desc => next_description(:reset)}.merge(options), &block) | |
101 | ||
102 | define_task(task) | |
103 | end | |
104 | ||
105 | def define_task(task) | |
106 | tasks[task.name] = task | |
107 | ||
108 | metaclass = class << self; self; end | |
109 | metaclass.send(:define_method, task.name) { execute_task(tasks[task.name]) } | |
110 | end | |
111 | ||
112 | # Find the task with the given name, where name is the fully-qualified | |
113 | # name of the task. This will search into the namespaces and return | |
114 | # the referenced task, or nil if no such task can be found. If the name | |
115 | # refers to a namespace, the task in that namespace named "default" | |
116 | # will be returned instead, if one exists. | |
117 | def find_task(name) | |
118 | parts = name.to_s.split(/:/) | |
119 | tail = parts.pop.to_sym | |
120 | ||
121 | ns = self | |
122 | until parts.empty? | |
123 | next_part = parts.shift | |
124 | ns = next_part.empty? ? nil : ns.namespaces[next_part.to_sym] | |
125 | return nil if ns.nil? | |
126 | end | |
127 | ||
128 | if ns.namespaces.key?(tail) | |
129 | ns = ns.namespaces[tail] | |
130 | tail = DEFAULT_TASK | |
131 | end | |
132 | ||
133 | ns.tasks[tail] | |
134 | end | |
135 | ||
136 | # Given a task name, this will search the current namespace, and all | |
137 | # parent namespaces, looking for a task that matches the name, exactly. | |
138 | # It returns the task, if found, or nil, if not. | |
139 | def search_task(name) | |
140 | name = name.to_sym | |
141 | ns = self | |
142 | ||
143 | until ns.nil? | |
144 | return ns.tasks[name] if ns.tasks.key?(name) | |
145 | ns = ns.parent | |
146 | end | |
147 | ||
148 | return nil | |
149 | end | |
150 | ||
151 | # Returns the default task for this namespace. This will be +nil+ if | |
152 | # the namespace is at the top-level, and will otherwise return the | |
153 | # task named "default". If no such task exists, +nil+ will be returned. | |
154 | def default_task | |
155 | return nil if parent.nil? | |
156 | return tasks[DEFAULT_TASK] | |
157 | end | |
158 | ||
159 | # Returns the tasks in this namespace as an array of TaskDefinition | |
160 | # objects. If a non-false parameter is given, all tasks in all | |
161 | # namespaces under this namespace will be returned as well. | |
162 | def task_list(all=false) | |
163 | list = tasks.values | |
164 | namespaces.each { |name,space| list.concat(space.task_list(:all)) } if all | |
165 | list | |
166 | end | |
167 | ||
168 | private | |
169 | ||
170 | def all_methods | |
171 | public_methods.concat(protected_methods).concat(private_methods) | |
172 | end | |
173 | ||
174 | class Namespace | |
175 | def initialize(name, parent) | |
176 | @parent = parent | |
177 | @name = name | |
178 | ||
179 | explicitly_define_clashing_kernel_methods | |
180 | end | |
181 | ||
182 | def role(*args) | |
183 | raise NotImplementedError, "roles cannot be defined in a namespace" | |
184 | end | |
185 | ||
186 | def respond_to?(sym, include_priv=false) | |
187 | super || parent.respond_to?(sym, include_priv) | |
188 | end | |
189 | ||
190 | def method_missing(sym, *args, &block) | |
191 | if parent.respond_to?(sym) | |
192 | parent.send(sym, *args, &block) | |
193 | else | |
194 | super | |
195 | end | |
196 | end | |
197 | ||
198 | include Capistrano::Configuration::AliasTask | |
199 | include Capistrano::Configuration::Namespaces | |
200 | undef :desc, :next_description | |
201 | ||
202 | protected | |
203 | def explicitly_define_clashing_kernel_methods | |
204 | (parent.public_methods & Kernel.methods).each do |m| | |
205 | next if self.method(m).owner == self.class | |
206 | if parent.method(m).owner == parent.class | |
207 | metaclass = class << self; self; end | |
208 | metaclass.send(:define_method, m) {|*args, &block| parent.send(m, *args, &block)} | |
209 | end | |
210 | end | |
211 | end | |
212 | end | |
213 | end | |
214 | end | |
215 | end |
0 | module Capistrano | |
1 | class Configuration | |
2 | class Question | |
3 | ||
4 | def initialize(env, key, default) | |
5 | @env, @key, @default = env, key, default | |
6 | end | |
7 | ||
8 | def call | |
9 | ask_question | |
10 | save_response | |
11 | end | |
12 | ||
13 | private | |
14 | attr_reader :env, :key, :default | |
15 | ||
16 | def ask_question | |
17 | $stdout.puts question | |
18 | end | |
19 | ||
20 | def save_response | |
21 | env.set(key, value) | |
22 | end | |
23 | ||
24 | def value | |
25 | if response.empty? | |
26 | default | |
27 | else | |
28 | response | |
29 | end | |
30 | end | |
31 | ||
32 | def response | |
33 | @response ||= $stdin.gets.chomp | |
34 | end | |
35 | ||
36 | def question | |
37 | I18n.t(:question, key: key, default_value: default, scope: :capistrano) | |
38 | end | |
39 | end | |
40 | end | |
41 | end |
0 | require 'capistrano/server_definition' | |
1 | require 'capistrano/role' | |
2 | ||
3 | module Capistrano | |
4 | class Configuration | |
5 | module Roles | |
6 | def self.included(base) #:nodoc: | |
7 | base.send :alias_method, :initialize_without_roles, :initialize | |
8 | base.send :alias_method, :initialize, :initialize_with_roles | |
9 | end | |
10 | ||
11 | # The hash of roles defined for this configuration. Each entry in the | |
12 | # hash points to an array of server definitions that belong in that | |
13 | # role. | |
14 | attr_reader :roles | |
15 | ||
16 | def initialize_with_roles(*args) #:nodoc: | |
17 | initialize_without_roles(*args) | |
18 | @roles = Hash.new { |h,k| h[k] = Role.new } | |
19 | end | |
20 | ||
21 | # Define a new role and its associated servers. You must specify at least | |
22 | # one host for each role. Also, you can specify additional information | |
23 | # (in the form of a Hash) which can be used to more uniquely specify the | |
24 | # subset of servers specified by this specific role definition. | |
25 | # | |
26 | # Usage: | |
27 | # | |
28 | # role :db, "db1.example.com", "db2.example.com" | |
29 | # role :db, "master.example.com", :primary => true | |
30 | # role :app, "app1.example.com", "app2.example.com" | |
31 | # | |
32 | # You can also encode the username and port number for each host in the | |
33 | # server string, if needed: | |
34 | # | |
35 | # role :web, "www@web1.example.com" | |
36 | # role :file, "files.example.com:4144" | |
37 | # role :db, "admin@db3.example.com:1234" | |
38 | # | |
39 | # Lastly, username and port number may be passed as options, if that is | |
40 | # preferred; note that the options apply to all servers defined in | |
41 | # that call to "role": | |
42 | # | |
43 | # role :web, "web2", "web3", :user => "www", :port => 2345 | |
44 | def role(which, *args, &block) | |
45 | options = args.last.is_a?(Hash) ? args.pop : {} | |
46 | which = which.to_sym | |
47 | ||
48 | # The roles Hash is defined so that unrecognized keys always auto-initialize | |
49 | # to a new Role instance (see the assignment in the initialize_with_roles method, | |
50 | # above). However, we explicitly assign here so that role declarations will | |
51 | # vivify the role object even if there are no server arguments. (Otherwise, | |
52 | # role(:app) won't actually instantiate a Role object for :app.) | |
53 | roles[which] ||= Role.new | |
54 | ||
55 | roles[which].push(block, options) if block_given? | |
56 | args.each { |host| roles[which] << ServerDefinition.new(host, options) } | |
57 | end | |
58 | ||
59 | # An alternative way to associate servers with roles. If you have a server | |
60 | # that participates in multiple roles, this can be a DRYer way to describe | |
61 | # the relationships. Pass the host definition as the first parameter, and | |
62 | # the roles as the remaining parameters: | |
63 | # | |
64 | # server "master.example.com", :web, :app | |
65 | def server(host, *roles) | |
66 | options = roles.last.is_a?(Hash) ? roles.pop : {} | |
67 | raise ArgumentError, "you must associate a server with at least one role" if roles.empty? | |
68 | roles.each { |name| role(name, host, options) } | |
69 | end | |
70 | ||
71 | def role_names_for_host(host) | |
72 | roles.map {|role_name, role| role_name if role.include?(host) }.compact || [] | |
73 | end | |
74 | end | |
75 | end | |
76 | end |
0 | require 'set' | |
1 | module Capistrano | |
2 | class Configuration | |
3 | class Server < SSHKit::Host | |
4 | extend Forwardable | |
5 | def_delegators :properties, :roles, :fetch, :set | |
6 | ||
7 | def self.[](host) | |
8 | host.is_a?(Server) ? host : new(host) | |
9 | end | |
10 | ||
11 | def add_roles(roles) | |
12 | Array(roles).each { |role| add_role(role) } | |
13 | end | |
14 | alias roles= add_roles | |
15 | ||
16 | def add_role(role) | |
17 | roles.add role.to_sym | |
18 | end | |
19 | ||
20 | def has_role?(role) | |
21 | roles.include? role.to_sym | |
22 | end | |
23 | ||
24 | def select?(options) | |
25 | selector = Selector.for(options) | |
26 | selector.call(self) | |
27 | end | |
28 | ||
29 | def primary | |
30 | self if fetch(:primary) | |
31 | end | |
32 | ||
33 | def with(properties) | |
34 | properties.each { |key, value| add_property(key, value) } | |
35 | self | |
36 | end | |
37 | ||
38 | def properties | |
39 | @properties ||= Properties.new | |
40 | end | |
41 | ||
42 | def netssh_options_with_options | |
43 | @netssh_options ||= netssh_options_without_options.merge( fetch(:ssh_options) || {} ) | |
44 | end | |
45 | alias_method :netssh_options_without_options, :netssh_options | |
46 | alias_method :netssh_options, :netssh_options_with_options | |
47 | ||
48 | def roles_array | |
49 | roles.to_a | |
50 | end | |
51 | ||
52 | def matches?(other) | |
53 | user == other.user && hostname == other.hostname && port == other.port | |
54 | end | |
55 | ||
56 | private | |
57 | ||
58 | def add_property(key, value) | |
59 | if respond_to?("#{key}=") | |
60 | send("#{key}=", value) | |
61 | else | |
62 | set(key, value) | |
63 | end | |
64 | end | |
65 | ||
66 | class Properties | |
67 | ||
68 | def initialize | |
69 | @properties = {} | |
70 | end | |
71 | ||
72 | def set(key, value) | |
73 | @properties[key] = value | |
74 | end | |
75 | ||
76 | def fetch(key) | |
77 | @properties[key] | |
78 | end | |
79 | ||
80 | def respond_to?(method) | |
81 | @properties.has_key?(method) | |
82 | end | |
83 | ||
84 | def roles | |
85 | @roles ||= Set.new | |
86 | end | |
87 | ||
88 | def keys | |
89 | @properties.keys | |
90 | end | |
91 | ||
92 | def method_missing(key, value=nil) | |
93 | if value | |
94 | set(lvalue(key), value) | |
95 | else | |
96 | fetch(key) | |
97 | end | |
98 | end | |
99 | ||
100 | private | |
101 | ||
102 | def lvalue(key) | |
103 | key.to_s.chomp('=').to_sym | |
104 | end | |
105 | ||
106 | end | |
107 | ||
108 | class Selector | |
109 | def initialize(options) | |
110 | @options = options | |
111 | end | |
112 | ||
113 | def self.for(options) | |
114 | if options.has_key?(:exclude) | |
115 | Exclusive | |
116 | else | |
117 | self | |
118 | end.new(options) | |
119 | end | |
120 | ||
121 | def callable | |
122 | if key.respond_to?(:call) | |
123 | key | |
124 | else | |
125 | ->(server) { server.fetch(key) } | |
126 | end | |
127 | end | |
128 | ||
129 | def call(server) | |
130 | callable.call(server) | |
131 | end | |
132 | ||
133 | private | |
134 | attr_reader :options | |
135 | ||
136 | def key | |
137 | options[:filter] || options[:select] || all | |
138 | end | |
139 | ||
140 | def all | |
141 | ->(server) { :all } | |
142 | end | |
143 | ||
144 | class Exclusive < Selector | |
145 | ||
146 | def key | |
147 | options[:exclude] | |
148 | end | |
149 | ||
150 | def call(server) | |
151 | !callable.call(server) | |
152 | end | |
153 | end | |
154 | ||
155 | end | |
156 | ||
157 | end | |
158 | end | |
159 | end |
0 | module Capistrano | |
1 | class Configuration | |
2 | class Servers | |
3 | class HostFilter | |
4 | ||
5 | def initialize(available) | |
6 | @available = available | |
7 | end | |
8 | ||
9 | def self.for(available) | |
10 | new(available).hosts | |
11 | end | |
12 | ||
13 | def hosts | |
14 | if host_filter.any? | |
15 | @available.select { |server| host_filter.include? server.hostname } | |
16 | else | |
17 | @available | |
18 | end | |
19 | end | |
20 | ||
21 | private | |
22 | ||
23 | def filter | |
24 | if host_filter.any? | |
25 | host_filter | |
26 | else | |
27 | @available | |
28 | end | |
29 | end | |
30 | ||
31 | def host_filter | |
32 | env_filter | configuration_filter | |
33 | end | |
34 | ||
35 | def configuration_filter | |
36 | ConfigurationFilter.new.hosts | |
37 | end | |
38 | ||
39 | def env_filter | |
40 | EnvFilter.new.hosts | |
41 | end | |
42 | ||
43 | class ConfigurationFilter | |
44 | ||
45 | def hosts | |
46 | if filter | |
47 | Array(filter.fetch(:hosts, [])) | |
48 | else | |
49 | [] | |
50 | end | |
51 | end | |
52 | ||
53 | def config | |
54 | Configuration.env | |
55 | end | |
56 | ||
57 | def filter | |
58 | config.fetch(:filter) || config.fetch(:select) | |
59 | end | |
60 | end | |
61 | ||
62 | ||
63 | class EnvFilter | |
64 | ||
65 | def hosts | |
66 | if filter | |
67 | filter.split(',') | |
68 | else | |
69 | [] | |
70 | end | |
71 | end | |
72 | ||
73 | def filter | |
74 | ENV['HOSTS'] | |
75 | end | |
76 | end | |
77 | ||
78 | end | |
79 | end | |
80 | end | |
81 | end |
0 | module Capistrano | |
1 | class Configuration | |
2 | class Servers | |
3 | class RoleFilter | |
4 | ||
5 | def initialize(required, available) | |
6 | @required, @available = required, available | |
7 | end | |
8 | ||
9 | def self.for(required, available) | |
10 | new(required, available).roles | |
11 | end | |
12 | ||
13 | def roles | |
14 | if required.include?(:all) | |
15 | available | |
16 | else | |
17 | required.select { |name| available.include? name } | |
18 | end | |
19 | end | |
20 | ||
21 | private | |
22 | ||
23 | def required | |
24 | Array(@required).flat_map(&:to_sym) | |
25 | end | |
26 | ||
27 | def available | |
28 | if role_filter.any? | |
29 | role_filter | |
30 | else | |
31 | @available | |
32 | end | |
33 | end | |
34 | ||
35 | def role_filter | |
36 | env_filter | configuration_filter | |
37 | end | |
38 | ||
39 | def configuration_filter | |
40 | ConfigurationFilter.new.roles | |
41 | end | |
42 | ||
43 | def env_filter | |
44 | EnvFilter.new.roles | |
45 | end | |
46 | ||
47 | class ConfigurationFilter | |
48 | ||
49 | def roles | |
50 | if filter | |
51 | Array(filter.fetch(:roles, [])).map(&:to_sym) | |
52 | else | |
53 | [] | |
54 | end | |
55 | end | |
56 | ||
57 | def config | |
58 | Configuration.env | |
59 | end | |
60 | ||
61 | def filter | |
62 | config.fetch(:filter) || config.fetch(:select) | |
63 | end | |
64 | end | |
65 | ||
66 | ||
67 | class EnvFilter | |
68 | ||
69 | def roles | |
70 | if filter | |
71 | filter.split(',').map(&:to_sym) | |
72 | else | |
73 | [] | |
74 | end | |
75 | end | |
76 | ||
77 | def filter | |
78 | ENV['ROLES'] | |
79 | end | |
80 | end | |
81 | ||
82 | end | |
83 | end | |
84 | end | |
85 | end |
0 | require 'set' | |
1 | require_relative 'servers/role_filter' | |
2 | require_relative 'servers/host_filter' | |
0 | 3 | module Capistrano |
1 | 4 | class Configuration |
2 | module Servers | |
3 | # Identifies all servers that the given task should be executed on. | |
4 | # The options hash accepts the same arguments as #find_servers, and any | |
5 | # preexisting options there will take precedence over the options in | |
6 | # the task. | |
7 | def find_servers_for_task(task, options={}) | |
8 | find_servers(task.options.merge(options)) | |
5 | class Servers | |
6 | include Enumerable | |
7 | ||
8 | def add_host(host, properties={}) | |
9 | servers.add server(host).with(properties) | |
9 | 10 | end |
10 | 11 | |
11 | # Attempts to find all defined servers that match the given criteria. | |
12 | # The options hash may include a :hosts option (which should specify | |
13 | # an array of host names or ServerDefinition instances), a :roles | |
14 | # option (specifying an array of roles), an :only option (specifying | |
15 | # a hash of key/value pairs that any matching server must match), | |
16 | # an :exception option (like :only, but the inverse), and a | |
17 | # :skip_hostfilter option to ignore the HOSTFILTER environment variable | |
18 | # described below. | |
19 | # | |
20 | # Additionally, if the HOSTS environment variable is set, it will take | |
21 | # precedence over any other options. Similarly, the ROLES environment | |
22 | # variable will take precedence over other options. If both HOSTS and | |
23 | # ROLES are given, HOSTS wins. | |
24 | # | |
25 | # Yet additionally, if the HOSTFILTER environment variable is set, it | |
26 | # will limit the result to hosts found in that (comma-separated) list. | |
27 | # | |
28 | # If the HOSTROLEFILTER environment variable is set, it will limit the | |
29 | # result to hosts found in that (comma-separated) list of roles | |
30 | # | |
31 | # Usage: | |
32 | # | |
33 | # # return all known servers | |
34 | # servers = find_servers | |
35 | # | |
36 | # # find all servers in the app role that are not exempted from | |
37 | # # deployment | |
38 | # servers = find_servers :roles => :app, | |
39 | # :except => { :no_release => true } | |
40 | # | |
41 | # # returns the given hosts, translated to ServerDefinition objects | |
42 | # servers = find_servers :hosts => "jamis@example.host.com" | |
43 | def find_servers(options={}) | |
44 | return [] if options.key?(:hosts) && (options[:hosts].nil? || [] == options[:hosts]) | |
45 | return [] if options.key?(:roles) && (options[:roles].nil? || [] == options[:roles]) | |
46 | ||
47 | hosts = server_list_from(ENV['HOSTS'] || options[:hosts]) | |
48 | ||
49 | if hosts.any? | |
50 | if options[:skip_hostfilter] | |
51 | hosts.uniq | |
52 | else | |
53 | filter_server_list(hosts.uniq) | |
54 | end | |
55 | else | |
56 | roles = role_list_from(ENV['ROLES'] || options[:roles] || self.roles.keys) | |
57 | roles = roles & Array(options[:roles]) if preserve_roles && !options[:roles].nil? | |
58 | ||
59 | only = options[:only] || {} | |
60 | except = options[:except] || {} | |
61 | ||
62 | # If we don't have a def for a role it means its bogus, skip it so higher level can handle | |
63 | servers = roles.inject([]) { |list, role| list.concat(self.roles[role] || []) } | |
64 | servers = servers.select { |server| only.all? { |key,value| server.options[key] == value } } | |
65 | servers = servers.reject { |server| except.any? { |key,value| server.options[key] == value } } | |
66 | ||
67 | if options[:skip_hostfilter] | |
68 | servers.uniq | |
69 | else | |
70 | filter_server_list(servers.uniq) | |
71 | end | |
72 | end | |
12 | def add_role(role, hosts, options={}) | |
13 | Array(hosts).each { |host| add_host(host, options.merge(roles: role)) } | |
73 | 14 | end |
74 | 15 | |
75 | protected | |
76 | ||
77 | def filter_server_list(servers) | |
78 | return servers unless ENV['HOSTFILTER'] or ENV['HOSTROLEFILTER'] | |
79 | if ENV['HOSTFILTER'] | |
80 | filters = ENV['HOSTFILTER'].split(/,/) | |
81 | servers.select { |server| filters.include?(server.host) } | |
82 | elsif ENV['HOSTROLEFILTER'] | |
83 | filters = ENV['HOSTROLEFILTER'].split(/,/).map do |role| | |
84 | local_roles = roles[role.to_sym] | |
85 | if local_roles.is_a? Array | |
86 | roles[role.to_sym] | |
87 | else | |
88 | roles[role.to_sym].servers | |
89 | end | |
90 | end.flatten | |
91 | servers.select { |server| filters.include?(server) } | |
92 | end | |
16 | def roles_for(names) | |
17 | options = extract_options(names) | |
18 | fetch_roles(names, options) | |
93 | 19 | end |
94 | 20 | |
95 | def server_list_from(hosts) | |
96 | hosts = hosts.split(/,/) if String === hosts | |
97 | hosts = build_list(hosts) | |
98 | hosts.map { |s| String === s ? ServerDefinition.new(s.strip) : s } | |
21 | def fetch_primary(role) | |
22 | hosts = fetch(role) | |
23 | hosts.find(&:primary) || hosts.first | |
99 | 24 | end |
100 | 25 | |
101 | def role_list_from(roles) | |
102 | roles = roles.split(/,/) if String === roles | |
103 | roles = build_list(roles) | |
104 | roles.map do |role| | |
105 | role = String === role ? role.strip.to_sym : role | |
106 | role | |
107 | end | |
26 | def each | |
27 | servers.each { |server| yield server } | |
108 | 28 | end |
109 | 29 | |
110 | def build_list(list) | |
111 | Array(list).map { |item| item.respond_to?(:call) ? item.call : item }.flatten | |
30 | private | |
31 | ||
32 | def server(host) | |
33 | servers.find { |server| server.matches? Server[host] } || Server[host] | |
34 | end | |
35 | ||
36 | def fetch(role) | |
37 | servers.find_all { |server| server.has_role? role} | |
38 | end | |
39 | ||
40 | def fetch_roles(required, options) | |
41 | filter_roles = RoleFilter.for(required, available_roles) | |
42 | HostFilter.for(select(servers_with_roles(filter_roles), options)) | |
43 | end | |
44 | ||
45 | def servers_with_roles(roles) | |
46 | roles.flat_map { |role| fetch role }.uniq | |
47 | end | |
48 | ||
49 | def select(servers, options) | |
50 | servers.select { |server| server.select?(options) } | |
51 | end | |
52 | ||
53 | def available_roles | |
54 | servers.flat_map { |server| server.roles_array }.uniq | |
55 | end | |
56 | ||
57 | def servers | |
58 | @servers ||= Set.new | |
59 | end | |
60 | ||
61 | def extract_options(array) | |
62 | array.last.is_a?(::Hash) ? array.pop : {} | |
112 | 63 | end |
113 | 64 | end |
114 | 65 | end |
0 | require 'thread' | |
1 | ||
2 | module Capistrano | |
3 | class Configuration | |
4 | module Variables | |
5 | def self.included(base) #:nodoc: | |
6 | %w(initialize respond_to? method_missing).each do |m| | |
7 | base_name = m[/^\w+/] | |
8 | punct = m[/\W+$/] | |
9 | base.send :alias_method, "#{base_name}_without_variables#{punct}", m | |
10 | base.send :alias_method, m, "#{base_name}_with_variables#{punct}" | |
11 | end | |
12 | end | |
13 | ||
14 | # The hash of variables that have been defined in this configuration | |
15 | # instance. | |
16 | attr_reader :variables | |
17 | ||
18 | # Set a variable to the given value. | |
19 | def set(variable, *args, &block) | |
20 | if variable.to_s !~ /^[_a-z]/ | |
21 | raise ArgumentError, "invalid variable `#{variable}' (variables must begin with an underscore, or a lower-case letter)" | |
22 | end | |
23 | ||
24 | if !block_given? && args.empty? || block_given? && !args.empty? | |
25 | raise ArgumentError, "you must specify exactly one of either a value or a block" | |
26 | end | |
27 | ||
28 | if args.length > 1 | |
29 | raise ArgumentError, "wrong number of arguments (#{args.length} for 1)" | |
30 | end | |
31 | ||
32 | value = args.empty? ? block : args.first | |
33 | sym = variable.to_sym | |
34 | protect(sym) { @variables[sym] = value } | |
35 | end | |
36 | ||
37 | alias :[]= :set | |
38 | ||
39 | # Removes any trace of the given variable. | |
40 | def unset(variable) | |
41 | sym = variable.to_sym | |
42 | protect(sym) do | |
43 | @original_procs.delete(sym) | |
44 | @variables.delete(sym) | |
45 | end | |
46 | end | |
47 | ||
48 | # Returns true if the variable has been defined, and false otherwise. | |
49 | def exists?(variable) | |
50 | @variables.key?(variable.to_sym) | |
51 | end | |
52 | ||
53 | # If the variable was originally a proc value, it will be reset to it's | |
54 | # original proc value. Otherwise, this method does nothing. It returns | |
55 | # true if the variable was actually reset. | |
56 | def reset!(variable) | |
57 | sym = variable.to_sym | |
58 | protect(sym) do | |
59 | if @original_procs.key?(sym) | |
60 | @variables[sym] = @original_procs.delete(sym) | |
61 | true | |
62 | else | |
63 | false | |
64 | end | |
65 | end | |
66 | end | |
67 | ||
68 | # Access a named variable. If the value of the variable responds_to? :call, | |
69 | # #call will be invoked (without parameters) and the return value cached | |
70 | # and returned. | |
71 | def fetch(variable, *args) | |
72 | if !args.empty? && block_given? | |
73 | raise ArgumentError, "you must specify either a default value or a block, but not both" | |
74 | end | |
75 | ||
76 | sym = variable.to_sym | |
77 | protect(sym) do | |
78 | if !@variables.key?(sym) | |
79 | return args.first unless args.empty? | |
80 | return yield(variable) if block_given? | |
81 | raise IndexError, "`#{variable}' not found" | |
82 | end | |
83 | ||
84 | if @variables[sym].respond_to?(:call) | |
85 | @original_procs[sym] = @variables[sym] | |
86 | @variables[sym] = @variables[sym].call | |
87 | end | |
88 | end | |
89 | ||
90 | @variables[sym] | |
91 | end | |
92 | ||
93 | def [](variable) | |
94 | fetch(variable, nil) | |
95 | end | |
96 | ||
97 | def initialize_with_variables(*args) #:nodoc: | |
98 | initialize_without_variables(*args) | |
99 | @variables = {} | |
100 | @original_procs = {} | |
101 | @variable_locks = Hash.new { |h,k| h[k] = Mutex.new } | |
102 | ||
103 | set :ssh_options, {} | |
104 | set :logger, logger | |
105 | end | |
106 | private :initialize_with_variables | |
107 | ||
108 | def protect(variable) | |
109 | @variable_locks[variable.to_sym].synchronize { yield } | |
110 | end | |
111 | private :protect | |
112 | ||
113 | def respond_to_with_variables?(sym, include_priv=false) #:nodoc: | |
114 | @variables.has_key?(sym.to_sym) || respond_to_without_variables?(sym, include_priv) | |
115 | end | |
116 | ||
117 | def method_missing_with_variables(sym, *args, &block) #:nodoc: | |
118 | if args.length == 0 && block.nil? && @variables.has_key?(sym) | |
119 | self[sym] | |
120 | else | |
121 | method_missing_without_variables(sym, *args, &block) | |
122 | end | |
123 | end | |
124 | end | |
125 | end | |
126 | end |
0 | require 'capistrano/logger' | |
1 | ||
2 | require 'capistrano/configuration/alias_task' | |
3 | require 'capistrano/configuration/callbacks' | |
4 | require 'capistrano/configuration/connections' | |
5 | require 'capistrano/configuration/execution' | |
6 | require 'capistrano/configuration/loading' | |
7 | require 'capistrano/configuration/namespaces' | |
8 | require 'capistrano/configuration/roles' | |
9 | require 'capistrano/configuration/servers' | |
10 | require 'capistrano/configuration/variables' | |
11 | ||
12 | require 'capistrano/configuration/actions/file_transfer' | |
13 | require 'capistrano/configuration/actions/inspect' | |
14 | require 'capistrano/configuration/actions/invocation' | |
0 | require_relative 'configuration/question' | |
1 | require_relative 'configuration/servers' | |
2 | require_relative 'configuration/server' | |
15 | 3 | |
16 | 4 | module Capistrano |
17 | # Represents a specific Capistrano configuration. A Configuration instance | |
18 | # may be used to load multiple recipe files, define and describe tasks, | |
19 | # define roles, and set configuration variables. | |
20 | 5 | class Configuration |
21 | # The logger instance defined for this configuration. | |
22 | attr_accessor :debug, :logger, :dry_run, :preserve_roles | |
23 | 6 | |
24 | def initialize(options={}) #:nodoc: | |
25 | @debug = false | |
26 | @dry_run = false | |
27 | @preserve_roles = false | |
28 | @logger = Logger.new(options) | |
7 | class << self | |
8 | def env | |
9 | @env ||= new | |
10 | end | |
11 | ||
12 | def reset! | |
13 | @env = new | |
14 | end | |
29 | 15 | end |
30 | 16 | |
31 | # make the DSL easier to read when using lazy evaluation via lambdas | |
32 | alias defer lambda | |
17 | def ask(key, default=nil) | |
18 | question = Question.new(self, key, default) | |
19 | set(key, question) | |
20 | end | |
33 | 21 | |
34 | # The includes must come at the bottom, since they may redefine methods | |
35 | # defined in the base class. | |
36 | include AliasTask, Connections, Execution, Loading, Namespaces, Roles, Servers, Variables | |
22 | def set(key, value) | |
23 | config[key] = value | |
24 | end | |
37 | 25 | |
38 | # Mix in the actions | |
39 | include Actions::FileTransfer, Actions::Inspect, Actions::Invocation | |
26 | def delete(key) | |
27 | config.delete(key) | |
28 | end | |
40 | 29 | |
41 | # Must mix last, because it hooks into previously defined methods | |
42 | include Callbacks | |
30 | def fetch(key, default=nil, &block) | |
31 | value = fetch_for(key, default, &block) | |
32 | while callable_without_parameters?(value) | |
33 | value = set(key, value.call) | |
34 | end | |
35 | return value | |
36 | end | |
37 | ||
38 | def role(name, hosts, options={}) | |
39 | if name == :all | |
40 | raise ArgumentError.new("#{name} reserved name for role. Please choose another name") | |
41 | end | |
42 | ||
43 | servers.add_role(name, hosts, options) | |
44 | end | |
45 | ||
46 | def server(name, properties={}) | |
47 | servers.add_host(name, properties) | |
48 | end | |
49 | ||
50 | def roles_for(names) | |
51 | servers.roles_for(names) | |
52 | end | |
53 | ||
54 | def primary(role) | |
55 | servers.fetch_primary(role) | |
56 | end | |
57 | ||
58 | def backend | |
59 | @backend ||= SSHKit | |
60 | end | |
61 | ||
62 | attr_writer :backend | |
63 | ||
64 | def configure_backend | |
65 | backend.configure do |sshkit| | |
66 | sshkit.format = fetch(:format) | |
67 | sshkit.output_verbosity = fetch(:log_level) | |
68 | sshkit.default_env = fetch(:default_env) | |
69 | sshkit.backend = fetch(:sshkit_backend, SSHKit::Backend::Netssh) | |
70 | sshkit.backend.configure do |backend| | |
71 | backend.pty = fetch(:pty) | |
72 | backend.connection_timeout = fetch(:connection_timeout) | |
73 | backend.ssh_options = fetch(:ssh_options) if fetch(:ssh_options) | |
74 | end | |
75 | end | |
76 | end | |
77 | ||
78 | def timestamp | |
79 | @timestamp ||= Time.now.utc | |
80 | end | |
81 | ||
82 | private | |
83 | ||
84 | def servers | |
85 | @servers ||= Servers.new | |
86 | end | |
87 | ||
88 | def config | |
89 | @config ||= Hash.new | |
90 | end | |
91 | ||
92 | def fetch_for(key, default, &block) | |
93 | if block_given? | |
94 | config.fetch(key, &block) | |
95 | else | |
96 | config.fetch(key, default) | |
97 | end | |
98 | end | |
99 | ||
100 | def callable_without_parameters?(x) | |
101 | x.respond_to?(:call) && ( !x.respond_to?(:arity) || x.arity == 0) | |
102 | end | |
43 | 103 | end |
44 | 104 | end |
0 | load File.expand_path("../tasks/console.rake", __FILE__) |
0 | set :scm, :git | |
1 | set :branch, :master | |
2 | set :deploy_to, -> { "/var/www/#{fetch(:application)}" } | |
3 | set :tmp_dir, "/tmp" | |
4 | ||
5 | set :default_env, {} | |
6 | set :keep_releases, 5 | |
7 | ||
8 | set :format, :pretty | |
9 | set :log_level, :debug | |
10 | ||
11 | set :pty, false |
0 | module Capistrano | |
1 | module DSL | |
2 | module Env | |
3 | ||
4 | def configure_backend | |
5 | env.configure_backend | |
6 | end | |
7 | ||
8 | def fetch(key, default=nil, &block) | |
9 | env.fetch(key, default, &block) | |
10 | end | |
11 | ||
12 | def any?(key) | |
13 | value = fetch(key) | |
14 | if value && value.respond_to?(:any?) | |
15 | value.any? | |
16 | else | |
17 | !fetch(key).nil? | |
18 | end | |
19 | end | |
20 | ||
21 | def set(key, value) | |
22 | env.set(key, value) | |
23 | end | |
24 | ||
25 | def delete(key) | |
26 | env.delete(key) | |
27 | end | |
28 | ||
29 | def ask(key, value) | |
30 | env.ask(key, value) | |
31 | end | |
32 | ||
33 | def role(name, servers, options={}) | |
34 | env.role(name, servers, options) | |
35 | end | |
36 | ||
37 | def server(name, properties={}) | |
38 | env.server(name, properties) | |
39 | end | |
40 | ||
41 | def roles(*names) | |
42 | env.roles_for(names.flatten) | |
43 | end | |
44 | ||
45 | def release_roles(*names) | |
46 | options = { exclude: :no_release } | |
47 | if names.last.is_a? Hash | |
48 | names.last.merge(options) | |
49 | else | |
50 | names << options | |
51 | end | |
52 | roles(*names) | |
53 | end | |
54 | ||
55 | def primary(role) | |
56 | env.primary(role) | |
57 | end | |
58 | ||
59 | def env | |
60 | Configuration.env | |
61 | end | |
62 | ||
63 | def release_timestamp | |
64 | env.timestamp.strftime("%Y%m%d%H%M%S") | |
65 | end | |
66 | ||
67 | def asset_timestamp | |
68 | env.timestamp.strftime("%Y%m%d%H%M.%S") | |
69 | end | |
70 | ||
71 | end | |
72 | end | |
73 | end |
0 | require 'pathname' | |
1 | module Capistrano | |
2 | module DSL | |
3 | module Paths | |
4 | ||
5 | def deploy_to | |
6 | fetch(:deploy_to) | |
7 | end | |
8 | ||
9 | def deploy_path | |
10 | Pathname.new(deploy_to) | |
11 | end | |
12 | ||
13 | def current_path | |
14 | deploy_path.join('current') | |
15 | end | |
16 | ||
17 | def releases_path | |
18 | deploy_path.join('releases') | |
19 | end | |
20 | ||
21 | def release_path | |
22 | fetch(:release_path, current_path) | |
23 | end | |
24 | ||
25 | def set_release_path(timestamp=now) | |
26 | set(:release_timestamp, timestamp) | |
27 | set(:release_path, releases_path.join(timestamp)) | |
28 | end | |
29 | ||
30 | def stage_config_path | |
31 | Pathname.new fetch(:stage_config_path, 'config/deploy') | |
32 | end | |
33 | ||
34 | def deploy_config_path | |
35 | Pathname.new fetch(:deploy_config_path, 'config/deploy.rb') | |
36 | end | |
37 | ||
38 | def repo_url | |
39 | require 'cgi' | |
40 | require 'uri' | |
41 | if fetch(:git_http_username) and fetch(:git_http_password) | |
42 | URI.parse(fetch(:repo_url)).tap do |repo_uri| | |
43 | repo_uri.user = fetch(:git_http_username) | |
44 | repo_uri.password = CGI.escape(fetch(:git_http_password)) | |
45 | end.to_s | |
46 | elsif fetch(:git_http_username) | |
47 | URI.parse(fetch(:repo_url)).tap do |repo_uri| | |
48 | repo_uri.user = fetch(:git_http_username) | |
49 | end.to_s | |
50 | else | |
51 | fetch(:repo_url) | |
52 | end | |
53 | end | |
54 | ||
55 | def repo_path | |
56 | deploy_path.join('repo') | |
57 | end | |
58 | ||
59 | def shared_path | |
60 | deploy_path.join('shared') | |
61 | end | |
62 | ||
63 | def revision_log | |
64 | deploy_path.join('revisions.log') | |
65 | end | |
66 | ||
67 | def now | |
68 | env.timestamp.strftime("%Y%m%d%H%M%S") | |
69 | end | |
70 | ||
71 | def asset_timestamp | |
72 | env.timestamp.strftime("%Y%m%d%H%M.%S") | |
73 | end | |
74 | ||
75 | def linked_dirs(parent) | |
76 | paths = fetch(:linked_dirs) | |
77 | join_paths(parent, paths) | |
78 | end | |
79 | ||
80 | def linked_files(parent) | |
81 | paths = fetch(:linked_files) | |
82 | join_paths(parent, paths) | |
83 | end | |
84 | ||
85 | def linked_file_dirs(parent) | |
86 | map_dirnames(linked_files(parent)) | |
87 | end | |
88 | ||
89 | def linked_dir_parents(parent) | |
90 | map_dirnames(linked_dirs(parent)) | |
91 | end | |
92 | ||
93 | def join_paths(parent, paths) | |
94 | paths.map { |path| parent.join(path) } | |
95 | end | |
96 | ||
97 | def map_dirnames(paths) | |
98 | paths.map { |path| path.dirname } | |
99 | end | |
100 | end | |
101 | end | |
102 | end |
0 | module Capistrano | |
1 | module DSL | |
2 | module Stages | |
3 | ||
4 | def stages | |
5 | Dir[stage_definitions].map { |f| File.basename(f, '.rb') } | |
6 | end | |
7 | ||
8 | def stage_definitions | |
9 | stage_config_path.join('*.rb') | |
10 | end | |
11 | ||
12 | def stage_set? | |
13 | !!fetch(:stage, false) | |
14 | end | |
15 | ||
16 | end | |
17 | end | |
18 | end |
0 | module Capistrano | |
1 | module TaskEnhancements | |
2 | def before(task, prerequisite, *args, &block) | |
3 | prerequisite = Rake::Task.define_task(prerequisite, *args, &block) if block_given? | |
4 | Rake::Task[task].enhance do | |
5 | Rake::Task[prerequisite].invoke(*args) | |
6 | end | |
7 | end | |
8 | ||
9 | def after(task, post_task, *args, &block) | |
10 | Rake::Task.define_task(post_task, *args, &block) if block_given? | |
11 | post_task = Rake::Task[post_task] | |
12 | Rake::Task[task].enhance do | |
13 | post_task.invoke(args) | |
14 | end | |
15 | end | |
16 | ||
17 | def remote_file(task) | |
18 | target_roles = task.delete(:roles) { :all } | |
19 | define_remote_file_task(task, target_roles) | |
20 | end | |
21 | ||
22 | def define_remote_file_task(task, target_roles) | |
23 | Rake::Task.define_task(task) do |t| | |
24 | prerequisite_file = t.prerequisites.first | |
25 | file = shared_path.join(t.name) | |
26 | ||
27 | on roles(target_roles) do | |
28 | unless test "[ -f #{file} ]" | |
29 | info "Uploading #{prerequisite_file} to #{file}" | |
30 | upload! File.open(prerequisite_file), file | |
31 | end | |
32 | end | |
33 | ||
34 | end | |
35 | end | |
36 | ||
37 | def ensure_stage | |
38 | Rake::Task.define_task(:ensure_stage) do | |
39 | unless stage_set? | |
40 | puts t(:stage_not_set) | |
41 | exit 1 | |
42 | end | |
43 | end | |
44 | end | |
45 | ||
46 | def tasks_without_stage_dependency | |
47 | stages + default_tasks | |
48 | end | |
49 | ||
50 | def default_tasks | |
51 | %w{install} | |
52 | end | |
53 | ||
54 | def exit_deploy_because_of_exception(ex) | |
55 | warn t(:deploy_failed, ex: ex.inspect) | |
56 | invoke 'deploy:failed' | |
57 | exit(false) | |
58 | end | |
59 | ||
60 | def deploying? | |
61 | fetch(:deploying, false) | |
62 | end | |
63 | ||
64 | end | |
65 | end |
0 | require 'capistrano/dsl/task_enhancements' | |
1 | require 'capistrano/dsl/paths' | |
2 | require 'capistrano/dsl/stages' | |
3 | require 'capistrano/dsl/env' | |
4 | ||
5 | module Capistrano | |
6 | module DSL | |
7 | include TaskEnhancements | |
8 | include Env | |
9 | include Paths | |
10 | include Stages | |
11 | ||
12 | def invoke(task, *args) | |
13 | Rake::Task[task].invoke(*args) | |
14 | end | |
15 | ||
16 | def t(key, options={}) | |
17 | I18n.t(key, options.merge(scope: :capistrano)) | |
18 | end | |
19 | ||
20 | def scm | |
21 | fetch(:scm) | |
22 | end | |
23 | ||
24 | def sudo(*args) | |
25 | execute :sudo, *args | |
26 | end | |
27 | ||
28 | def revision_log_message | |
29 | fetch(:revision_log_message, | |
30 | t(:revision_log_message, | |
31 | branch: fetch(:branch), | |
32 | user: local_user, | |
33 | sha: fetch(:current_revision), | |
34 | release: release_timestamp) | |
35 | ) | |
36 | end | |
37 | ||
38 | def rollback_log_message | |
39 | t(:rollback_log_message, user: local_user, release: fetch(:rollback_timestamp)) | |
40 | end | |
41 | ||
42 | def local_user | |
43 | `whoami` | |
44 | end | |
45 | ||
46 | def lock(locked_version) | |
47 | VersionValidator.new(locked_version).verify | |
48 | end | |
49 | end | |
50 | end | |
51 | self.extend Capistrano::DSL |
0 | module Capistrano | |
1 | ||
2 | Error = Class.new(RuntimeError) | |
3 | ||
4 | CaptureError = Class.new(Capistrano::Error) | |
5 | NoSuchTaskError = Class.new(Capistrano::Error) | |
6 | NoMatchingServersError = Class.new(Capistrano::Error) | |
7 | ||
8 | class RemoteError < Error | |
9 | attr_accessor :hosts | |
10 | end | |
11 | ||
12 | ConnectionError = Class.new(Capistrano::RemoteError) | |
13 | TransferError = Class.new(Capistrano::RemoteError) | |
14 | CommandError = Class.new(Capistrano::RemoteError) | |
15 | ||
16 | LocalArgumentError = Class.new(Capistrano::Error) | |
17 | ||
18 | end |
0 | require 'fileutils' | |
1 | ||
2 | unless Capistrano::Configuration.respond_to?(:instance) | |
3 | abort "capistrano/ext/multistage requires Capistrano 2" | |
4 | end | |
5 | ||
6 | Capistrano::Configuration.instance.load do | |
7 | location = fetch(:stage_dir, "config/deploy") | |
8 | ||
9 | unless exists?(:stages) | |
10 | set :stages, Dir["#{location}/*.rb"].map { |f| File.basename(f, ".rb") } | |
11 | end | |
12 | ||
13 | stages.each do |name| | |
14 | desc "Set the target stage to `#{name}'." | |
15 | task(name) do | |
16 | set :stage, name.to_sym | |
17 | load "#{location}/#{stage}" | |
18 | end | |
19 | end | |
20 | ||
21 | on :load do | |
22 | if stages.include?(ARGV.first) | |
23 | # Execute the specified stage so that recipes required in stage can contribute to task list | |
24 | find_and_execute_task(ARGV.first) if ARGV.any?{ |option| option =~ /-T|--tasks|-e|--explain/ } | |
25 | else | |
26 | # Execute the default stage so that recipes required in stage can contribute tasks | |
27 | find_and_execute_task(default_stage) if exists?(:default_stage) | |
28 | end | |
29 | end | |
30 | ||
31 | namespace :multistage do | |
32 | desc "[internal] Ensure that a stage has been selected." | |
33 | task :ensure do | |
34 | if !exists?(:stage) | |
35 | if exists?(:default_stage) | |
36 | logger.important "Defaulting to `#{default_stage}'" | |
37 | find_and_execute_task(default_stage) | |
38 | else | |
39 | abort "No stage specified. Please specify one of: #{stages.join(', ')} (e.g. `cap #{stages.first} #{ARGV.last}')" | |
40 | end | |
41 | end | |
42 | end | |
43 | ||
44 | desc "Stub out the staging config files." | |
45 | task :prepare do | |
46 | FileUtils.mkdir_p(location) | |
47 | stages.each do |name| | |
48 | file = File.join(location, name + ".rb") | |
49 | unless File.exists?(file) | |
50 | File.open(file, "w") do |f| | |
51 | f.puts "# #{name.upcase}-specific deployment configuration" | |
52 | f.puts "# please put general deployment config in config/deploy.rb" | |
53 | end | |
54 | end | |
55 | end | |
56 | end | |
57 | end | |
58 | ||
59 | on :start, "multistage:ensure", :except => stages + ['multistage:prepare'] | |
60 | ||
61 | end |
0 | module Capistrano | |
1 | class ExtensionProxy #:nodoc: | |
2 | def initialize(config, mod) | |
3 | @config = config | |
4 | extend(mod) | |
5 | end | |
6 | ||
7 | def method_missing(sym, *args, &block) | |
8 | @config.send(sym, *args, &block) | |
9 | end | |
10 | end | |
11 | ||
12 | # Holds the set of registered plugins, keyed by name (where the name is a | |
13 | # symbol). | |
14 | EXTENSIONS = {} | |
15 | ||
16 | # Register the given module as a plugin with the given name. It will henceforth | |
17 | # be available via a proxy object on Configuration instances, accessible by | |
18 | # a method with the given name. | |
19 | def self.plugin(name, mod) | |
20 | name = name.to_sym | |
21 | return false if EXTENSIONS.has_key?(name) | |
22 | ||
23 | methods = Capistrano::Configuration.public_instance_methods + | |
24 | Capistrano::Configuration.protected_instance_methods + | |
25 | Capistrano::Configuration.private_instance_methods | |
26 | ||
27 | if methods.any? { |m| m.to_sym == name } | |
28 | raise Capistrano::Error, "registering a plugin named `#{name}' would shadow a method on Capistrano::Configuration with the same name" | |
29 | end | |
30 | ||
31 | Capistrano::Configuration.class_eval <<-STR, __FILE__, __LINE__+1 | |
32 | def #{name} | |
33 | @__#{name}_proxy ||= Capistrano::ExtensionProxy.new(self, Capistrano::EXTENSIONS[#{name.inspect}]) | |
34 | end | |
35 | STR | |
36 | ||
37 | EXTENSIONS[name] = mod | |
38 | return true | |
39 | end | |
40 | ||
41 | # Unregister the plugin with the given name. | |
42 | def self.remove_plugin(name) | |
43 | name = name.to_sym | |
44 | if EXTENSIONS.delete(name) | |
45 | Capistrano::Configuration.send(:remove_method, name) | |
46 | return true | |
47 | end | |
48 | ||
49 | return false | |
50 | end | |
51 | ||
52 | def self.configuration(*args) #:nodoc: | |
53 | warn "[DEPRECATION] Capistrano.configuration is deprecated. Use Capistrano::Configuration.instance instead" | |
54 | Capistrano::Configuration.instance(*args) | |
55 | end | |
56 | end |
0 | # | |
1 | # See https://github.com/jimweirich/rake/issues/81 | |
2 | # | |
3 | if defined?(Rake::DeprecatedObjectDSL) | |
4 | Rake::DeprecatedObjectDSL.private_instance_methods.each do |m| | |
5 | Rake::DeprecatedObjectDSL.send("undef_method", m) | |
6 | end | |
7 | end |
0 | load File.expand_path("../tasks/git.rake", __FILE__) | |
1 | ||
2 | require 'capistrano/scm' | |
3 | ||
4 | class Capistrano::Git < Capistrano::SCM | |
5 | ||
6 | # execute git with argument in the context | |
7 | # | |
8 | def git(*args) | |
9 | args.unshift :git | |
10 | context.execute *args | |
11 | end | |
12 | ||
13 | # The Capistrano default strategy for git. You should want to use this. | |
14 | module DefaultStrategy | |
15 | def test | |
16 | test! " [ -f #{repo_path}/HEAD ] " | |
17 | end | |
18 | ||
19 | def check | |
20 | test! :git, :'ls-remote -h', repo_url | |
21 | end | |
22 | ||
23 | def clone | |
24 | git :clone, '--mirror', repo_url, repo_path | |
25 | end | |
26 | ||
27 | def update | |
28 | git :remote, :update | |
29 | end | |
30 | ||
31 | def release | |
32 | git :archive, fetch(:branch), '| tar -x -C', release_path | |
33 | end | |
34 | ||
35 | def fetch_revision | |
36 | context.capture(:git, "rev-parse #{fetch(:branch)}") | |
37 | end | |
38 | end | |
39 | end |
0 | load File.expand_path("../tasks/hg.rake", __FILE__) | |
1 | ||
2 | require 'capistrano/scm' | |
3 | ||
4 | class Capistrano::Hg < Capistrano::SCM | |
5 | # execute hg in context with arguments | |
6 | def hg(*args) | |
7 | args.unshift(:hg) | |
8 | context.execute *args | |
9 | end | |
10 | ||
11 | module DefaultStrategy | |
12 | def test | |
13 | test! " [ -d #{repo_path}/.hg ] " | |
14 | end | |
15 | ||
16 | def check | |
17 | hg "id", repo_url | |
18 | end | |
19 | ||
20 | def clone | |
21 | hg "clone", "--noupdate", repo_url, repo_path | |
22 | end | |
23 | ||
24 | def update | |
25 | hg "pull" | |
26 | end | |
27 | ||
28 | def release | |
29 | hg "archive", release_path, "--rev", fetch(:branch) | |
30 | end | |
31 | ||
32 | def fetch_revision | |
33 | context.capture(:hg, "log --rev #{fetch(:branch)} --template \"{node}\n\"") | |
34 | end | |
35 | end | |
36 | end |
0 | require 'i18n' | |
1 | ||
2 | en = { | |
3 | starting: 'Starting', | |
4 | capified: 'Capified', | |
5 | starting: 'Starting', | |
6 | start: 'Start', | |
7 | update: 'Update', | |
8 | finalize: 'Finalise', | |
9 | finishing: 'Finishing', | |
10 | finished: 'Finished', | |
11 | stage_not_set: 'Stage not set, please call something such as `cap production deploy`, where production is a stage you have defined.', | |
12 | written_file: 'create %{file}', | |
13 | question: 'Please enter %{key}: |%{default_value}|', | |
14 | keeping_releases: 'Keeping %{keep_releases} of %{releases} deployed releases on %{host}', | |
15 | no_old_releases: 'No old releases (keeping newest %{keep_releases}) on %{host}', | |
16 | linked_file_does_not_exist: 'linked file %{file} does not exist on %{host}', | |
17 | cannot_rollback: 'There are no older releases to rollback to', | |
18 | mirror_exists: "The repository mirror is at %{at}", | |
19 | revision_log_message: 'Branch %{branch} (at %{sha}) deployed as release %{release} by %{user}', | |
20 | rollback_log_message: '%{user} rolled back to release %{release}', | |
21 | deploy_failed: 'The deploy has failed with an error: %{ex}', | |
22 | console: { | |
23 | welcome: 'capistrano console - enter command to execute on %{stage}', | |
24 | bye: 'bye' | |
25 | }, | |
26 | error: { | |
27 | user: { | |
28 | does_not_exist: 'User %{user} does not exists', | |
29 | cannot_switch: 'Cannot switch to user %{user}' | |
30 | } | |
31 | } | |
32 | } | |
33 | ||
34 | I18n.backend.store_translations(:en, { capistrano: en }) | |
35 | ||
36 | if I18n.respond_to?(:enforce_available_locales=) | |
37 | I18n.enforce_available_locales = true | |
38 | end |
0 | load File.expand_path(File.join(File.dirname(__FILE__),'tasks/install.rake')) |
0 | module Capistrano | |
1 | class Logger #:nodoc: | |
2 | attr_accessor :level | |
3 | attr_reader :device | |
4 | ||
5 | IMPORTANT = 0 | |
6 | INFO = 1 | |
7 | DEBUG = 2 | |
8 | TRACE = 3 | |
9 | ||
10 | MAX_LEVEL = 3 | |
11 | ||
12 | def initialize(options={}) | |
13 | output = options[:output] || $stderr | |
14 | if output.respond_to?(:puts) | |
15 | @device = output | |
16 | else | |
17 | @device = File.open(output.to_str, "a") | |
18 | @needs_close = true | |
19 | end | |
20 | ||
21 | @options = options | |
22 | @level = 0 | |
23 | end | |
24 | ||
25 | def close | |
26 | device.close if @needs_close | |
27 | end | |
28 | ||
29 | def log(level, message, line_prefix=nil) | |
30 | if level <= self.level | |
31 | indent = "%*s" % [MAX_LEVEL, "*" * (MAX_LEVEL - level)] | |
32 | (RUBY_VERSION >= "1.9" ? message.lines : message).each do |line| | |
33 | if line_prefix | |
34 | device.puts "#{indent} [#{line_prefix}] #{line.strip}\n" | |
35 | else | |
36 | device.puts "#{indent} #{line.strip}\n" | |
37 | end | |
38 | end | |
39 | end | |
40 | end | |
41 | ||
42 | def important(message, line_prefix=nil) | |
43 | log(IMPORTANT, message, line_prefix) | |
44 | end | |
45 | ||
46 | def info(message, line_prefix=nil) | |
47 | log(INFO, message, line_prefix) | |
48 | end | |
49 | ||
50 | def debug(message, line_prefix=nil) | |
51 | log(DEBUG, message, line_prefix) | |
52 | end | |
53 | ||
54 | def trace(message, line_prefix=nil) | |
55 | log(TRACE, message, line_prefix) | |
56 | end | |
57 | end | |
58 | end |
0 | module Capistrano | |
1 | module Processable | |
2 | module SessionAssociation | |
3 | def self.on(exception, session) | |
4 | unless exception.respond_to?(:session) | |
5 | exception.extend(self) | |
6 | exception.session = session | |
7 | end | |
8 | ||
9 | return exception | |
10 | end | |
11 | ||
12 | attr_accessor :session | |
13 | end | |
14 | ||
15 | def process_iteration(wait=nil, &block) | |
16 | ensure_each_session { |session| session.preprocess } | |
17 | ||
18 | return false if block && !block.call(self) | |
19 | ||
20 | readers = sessions.map { |session| session.listeners.keys }.flatten.reject { |io| io.closed? } | |
21 | writers = readers.select { |io| io.respond_to?(:pending_write?) && io.pending_write? } | |
22 | ||
23 | if readers.any? || writers.any? | |
24 | readers, writers, = IO.select(readers, writers, nil, wait) | |
25 | end | |
26 | ||
27 | if readers | |
28 | ensure_each_session do |session| | |
29 | ios = session.listeners.keys | |
30 | session.postprocess(ios & readers, ios & writers) | |
31 | end | |
32 | end | |
33 | ||
34 | true | |
35 | end | |
36 | ||
37 | def ensure_each_session | |
38 | errors = [] | |
39 | ||
40 | sessions.each do |session| | |
41 | begin | |
42 | yield session | |
43 | rescue Exception => error | |
44 | errors << SessionAssociation.on(error, session) | |
45 | end | |
46 | end | |
47 | ||
48 | raise errors.first if errors.any? | |
49 | sessions | |
50 | end | |
51 | end | |
52 | end⏎ |
0 | # A collection of compatibility scripts, to ease the transition between | |
1 | # Capistrano 1.x and Capistrano 2.x. | |
2 | ||
3 | # Depends on the deployment system | |
4 | load 'deploy' | |
5 | ||
6 | map = { "diff_from_last_deploy" => "deploy:pending:diff", | |
7 | "update" => "deploy:update", | |
8 | "update_code" => "deploy:update_code", | |
9 | "symlink" => "deploy:create_symlink", | |
10 | "restart" => "deploy:restart", | |
11 | "rollback" => "deploy:rollback", | |
12 | "cleanup" => "deploy:cleanup", | |
13 | "disable_web" => "deploy:web:disable", | |
14 | "enable_web" => "deploy:web:enable", | |
15 | "cold_deploy" => "deploy:cold", | |
16 | "deploy_with_migrations" => "deploy:migrations" } | |
17 | ||
18 | map.each do |old, new| | |
19 | desc "DEPRECATED: See #{new}." | |
20 | eval "task(#{old.inspect}) do | |
21 | warn \"[DEPRECATED] `#{old}' is deprecated. Use `#{new}' instead.\" | |
22 | find_and_execute_task(#{new.inspect}) | |
23 | end" | |
24 | end | |
25 | ||
26 | desc "DEPRECATED: See deploy:start." | |
27 | task :spinner do | |
28 | warn "[DEPRECATED] `spinner' is deprecated. Use `deploy:start' instead." | |
29 | set :runner, fetch(:spinner_user, "app") | |
30 | deploy.start | |
31 | end |
0 | load 'deploy' unless defined?(_cset) | |
1 | ||
2 | _cset :asset_env, "RAILS_GROUPS=assets" | |
3 | _cset :assets_prefix, "assets" | |
4 | _cset :assets_role, [:web] | |
5 | ||
6 | _cset :normalize_asset_timestamps, false | |
7 | ||
8 | before 'deploy:finalize_update', 'deploy:assets:symlink' | |
9 | after 'deploy:update_code', 'deploy:assets:precompile' | |
10 | ||
11 | namespace :deploy do | |
12 | namespace :assets do | |
13 | desc <<-DESC | |
14 | [internal] This task will set up a symlink to the shared directory \ | |
15 | for the assets directory. Assets are shared across deploys to avoid \ | |
16 | mid-deploy mismatches between old application html asking for assets \ | |
17 | and getting a 404 file not found error. The assets cache is shared \ | |
18 | for efficiency. If you cutomize the assets path prefix, override the \ | |
19 | :assets_prefix variable to match. | |
20 | DESC | |
21 | task :symlink, :roles => assets_role, :except => { :no_release => true } do | |
22 | run <<-CMD | |
23 | rm -rf #{latest_release}/public/#{assets_prefix} && | |
24 | mkdir -p #{latest_release}/public && | |
25 | mkdir -p #{shared_path}/assets && | |
26 | ln -s #{shared_path}/assets #{latest_release}/public/#{assets_prefix} | |
27 | CMD | |
28 | end | |
29 | ||
30 | desc <<-DESC | |
31 | Run the asset precompilation rake task. You can specify the full path \ | |
32 | to the rake executable by setting the rake variable. You can also \ | |
33 | specify additional environment variables to pass to rake via the \ | |
34 | asset_env variable. The defaults are: | |
35 | ||
36 | set :rake, "rake" | |
37 | set :rails_env, "production" | |
38 | set :asset_env, "RAILS_GROUPS=assets" | |
39 | DESC | |
40 | task :precompile, :roles => assets_role, :except => { :no_release => true } do | |
41 | run "cd #{latest_release} && #{rake} RAILS_ENV=#{rails_env} #{asset_env} assets:precompile" | |
42 | end | |
43 | ||
44 | desc <<-DESC | |
45 | Run the asset clean rake task. Use with caution, this will delete \ | |
46 | all of your compiled assets. You can specify the full path \ | |
47 | to the rake executable by setting the rake variable. You can also \ | |
48 | specify additional environment variables to pass to rake via the \ | |
49 | asset_env variable. The defaults are: | |
50 | ||
51 | set :rake, "rake" | |
52 | set :rails_env, "production" | |
53 | set :asset_env, "RAILS_GROUPS=assets" | |
54 | DESC | |
55 | task :clean, :roles => assets_role, :except => { :no_release => true } do | |
56 | run "cd #{latest_release} && #{rake} RAILS_ENV=#{rails_env} #{asset_env} assets:clean" | |
57 | end | |
58 | end | |
59 | end |
0 | require 'capistrano/recipes/deploy/local_dependency' | |
1 | require 'capistrano/recipes/deploy/remote_dependency' | |
2 | ||
3 | module Capistrano | |
4 | module Deploy | |
5 | class Dependencies | |
6 | include Enumerable | |
7 | ||
8 | attr_reader :configuration | |
9 | ||
10 | def initialize(configuration) | |
11 | @configuration = configuration | |
12 | @dependencies = [] | |
13 | yield self if block_given? | |
14 | end | |
15 | ||
16 | def check | |
17 | yield self | |
18 | self | |
19 | end | |
20 | ||
21 | def remote | |
22 | dep = RemoteDependency.new(configuration) | |
23 | @dependencies << dep | |
24 | dep | |
25 | end | |
26 | ||
27 | def local | |
28 | dep = LocalDependency.new(configuration) | |
29 | @dependencies << dep | |
30 | dep | |
31 | end | |
32 | ||
33 | def each | |
34 | @dependencies.each { |d| yield d } | |
35 | self | |
36 | end | |
37 | ||
38 | def pass? | |
39 | all? { |d| d.pass? } | |
40 | end | |
41 | end | |
42 | end | |
43 | end⏎ |
0 | module Capistrano | |
1 | module Deploy | |
2 | class LocalDependency | |
3 | attr_reader :configuration | |
4 | attr_reader :message | |
5 | ||
6 | def initialize(configuration) | |
7 | @configuration = configuration | |
8 | @success = true | |
9 | end | |
10 | ||
11 | def command(command) | |
12 | @message ||= "`#{command}' could not be found in the path on the local host" | |
13 | @success = find_in_path(command) | |
14 | self | |
15 | end | |
16 | ||
17 | def or(message) | |
18 | @message = message | |
19 | self | |
20 | end | |
21 | ||
22 | def pass? | |
23 | @success | |
24 | end | |
25 | ||
26 | private | |
27 | ||
28 | # Searches the path, looking for the given utility. If an executable | |
29 | # file is found that matches the parameter, this returns true. | |
30 | def find_in_path(utility) | |
31 | path = (ENV['PATH'] || "").split(File::PATH_SEPARATOR) | |
32 | suffixes = self.class.on_windows? ? self.class.windows_executable_extensions : [""] | |
33 | ||
34 | path.each do |dir| | |
35 | suffixes.each do |sfx| | |
36 | file = File.join(dir, utility + sfx) | |
37 | return true if File.executable?(file) | |
38 | end | |
39 | end | |
40 | ||
41 | false | |
42 | end | |
43 | ||
44 | def self.on_windows? | |
45 | RUBY_PLATFORM =~ /mswin|mingw/ | |
46 | end | |
47 | ||
48 | def self.windows_executable_extensions | |
49 | %w(.exe .bat .com .cmd) | |
50 | end | |
51 | end | |
52 | end | |
53 | end |
0 | require 'capistrano/errors' | |
1 | ||
2 | module Capistrano | |
3 | module Deploy | |
4 | class RemoteDependency | |
5 | attr_reader :configuration | |
6 | attr_reader :hosts | |
7 | ||
8 | def initialize(configuration) | |
9 | @configuration = configuration | |
10 | @success = true | |
11 | @hosts = nil | |
12 | end | |
13 | ||
14 | def directory(path, options={}) | |
15 | @message ||= "`#{path}' is not a directory" | |
16 | try("test -d #{path}", options) | |
17 | self | |
18 | end | |
19 | ||
20 | def file(path, options={}) | |
21 | @message ||= "`#{path}' is not a file" | |
22 | try("test -f #{path}", options) | |
23 | self | |
24 | end | |
25 | ||
26 | def writable(path, options={}) | |
27 | @message ||= "`#{path}' is not writable" | |
28 | try("test -w #{path}", options) | |
29 | self | |
30 | end | |
31 | ||
32 | def command(command, options={}) | |
33 | @message ||= "`#{command}' could not be found in the path" | |
34 | try("which #{command}", options) | |
35 | self | |
36 | end | |
37 | ||
38 | def gem(name, version, options={}) | |
39 | @message ||= "gem `#{name}' #{version} could not be found" | |
40 | gem_cmd = configuration.fetch(:gem_command, "gem") | |
41 | try("#{gem_cmd} specification --version '#{version}' #{name} 2>&1 | awk 'BEGIN { s = 0 } /^name:/ { s = 1; exit }; END { if(s == 0) exit 1 }'", options) | |
42 | self | |
43 | end | |
44 | ||
45 | def deb(name, version, options={}) | |
46 | @message ||= "package `#{name}' #{version} could not be found" | |
47 | try("dpkg -s #{name} | grep '^Version: #{version}'", options) | |
48 | self | |
49 | end | |
50 | ||
51 | def rpm(name, version, options={}) | |
52 | @message ||= "package `#{name}' #{version} could not be found" | |
53 | try("rpm -q #{name} | grep '#{version}'", options) | |
54 | self | |
55 | end | |
56 | ||
57 | def match(command, expect, options={}) | |
58 | expect = Regexp.new(Regexp.escape(expect.to_s)) unless expect.is_a?(Regexp) | |
59 | ||
60 | output_per_server = {} | |
61 | try("#{command} ", options) do |ch, stream, out| | |
62 | output_per_server[ch[:server]] ||= '' | |
63 | output_per_server[ch[:server]] += out | |
64 | end | |
65 | ||
66 | # It is possible for some of these commands to return a status != 0 | |
67 | # (for example, rake --version exits with a 1). For this check we | |
68 | # just care if the output matches, so we reset the success flag. | |
69 | @success = true | |
70 | ||
71 | errored_hosts = [] | |
72 | output_per_server.each_pair do |server, output| | |
73 | next if output =~ expect | |
74 | errored_hosts << server | |
75 | end | |
76 | ||
77 | if errored_hosts.any? | |
78 | @hosts = errored_hosts.join(', ') | |
79 | output = output_per_server[errored_hosts.first] | |
80 | @message = "the output #{output.inspect} from #{command.inspect} did not match #{expect.inspect}" | |
81 | @success = false | |
82 | end | |
83 | ||
84 | self | |
85 | end | |
86 | ||
87 | def or(message) | |
88 | @message = message | |
89 | self | |
90 | end | |
91 | ||
92 | def pass? | |
93 | @success | |
94 | end | |
95 | ||
96 | def message | |
97 | s = @message.dup | |
98 | s << " (#{@hosts})" if @hosts | |
99 | s | |
100 | end | |
101 | ||
102 | private | |
103 | ||
104 | def try(command, options) | |
105 | return unless @success # short-circuit evaluation | |
106 | configuration.invoke_command(command, options) do |ch,stream,out| | |
107 | warn "#{ch[:server]}: #{out}" if stream == :err | |
108 | yield ch, stream, out if block_given? | |
109 | end | |
110 | rescue Capistrano::CommandError => e | |
111 | @success = false | |
112 | @hosts = e.hosts.join(', ') | |
113 | end | |
114 | end | |
115 | end | |
116 | end |
0 | require 'capistrano/recipes/deploy/scm/base' | |
1 | require 'rexml/xpath' | |
2 | require 'rexml/document' | |
3 | ||
4 | module Capistrano | |
5 | module Deploy | |
6 | module SCM | |
7 | # Accurev bridge for use by Capistrano. This implementation does not | |
8 | # implement all features of a Capistrano SCM module. The ones that are | |
9 | # left out are either exceedingly difficult to implement with Accurev | |
10 | # or are considered bad form. | |
11 | # | |
12 | # When using this module in a project, the following variables are used: | |
13 | # * :repository - This should match the depot that code lives in. If your code | |
14 | # exists in a subdirectory, you can append the path depot. | |
15 | # eg. foo-depot/bar_dir | |
16 | # * :stream - The stream in the depot that code should be pulled from. If | |
17 | # left blank, the depot stream will be used | |
18 | # * :revision - Should be in the form 'stream/transaction'. | |
19 | class Accurev < Base | |
20 | include REXML | |
21 | default_command 'accurev' | |
22 | ||
23 | # Defines pseudo-revision value for the most recent changes to be deployed. | |
24 | def head | |
25 | "#{stream}/highest" | |
26 | end | |
27 | ||
28 | # Given an Accurev revision identifier, this method returns an identifier that | |
29 | # can be used for later SCM calls. This returned identifier will not | |
30 | # change as a result of further SCM activity. | |
31 | def query_revision(revision) | |
32 | internal_revision = InternalRevision.parse(revision) | |
33 | return revision unless internal_revision.psuedo_revision? | |
34 | ||
35 | logger.debug("Querying for real revision for #{internal_revision}") | |
36 | rev_stream = internal_revision.stream | |
37 | ||
38 | logger.debug("Determining what type of stream #{rev_stream} is...") | |
39 | stream_xml = yield show_streams_for(rev_stream) | |
40 | stream_doc = Document.new(stream_xml) | |
41 | type = XPath.first(stream_doc, '//streams/stream/@type').value | |
42 | ||
43 | case type | |
44 | when 'snapshot' | |
45 | InternalRevision.new(rev_stream, 'highest').to_s | |
46 | else | |
47 | logger.debug("Getting latest transaction id in #{rev_stream}") | |
48 | # Doing another yield for a second Accurev call. Hopefully this is ok. | |
49 | hist_xml = yield scm(:hist, '-ftx', '-s', rev_stream, '-t', 'now.1') | |
50 | hist_doc = Document.new(hist_xml) | |
51 | transaction_id = XPath.first(hist_doc, '//AcResponse/transaction/@id').value | |
52 | InternalRevision.new(stream, transaction_id).to_s | |
53 | end | |
54 | end | |
55 | ||
56 | # Pops a copy of the code for the specified Accurev revision identifier. | |
57 | # The revision identifier is represented as a stream & transaction ID combo. | |
58 | # Accurev can only pop a particular transaction if a stream is created on the server | |
59 | # with a time basis of that transaction id. Therefore, we will create a stream with | |
60 | # the required criteria and pop that. | |
61 | def export(revision_id, destination) | |
62 | revision = InternalRevision.parse(revision_id) | |
63 | logger.debug("Exporting #{revision.stream}/#{revision.transaction_id} to #{destination}") | |
64 | ||
65 | commands = [ | |
66 | change_or_create_stream("#{revision.stream}-capistrano-deploy", revision), | |
67 | "mkdir -p #{destination}", | |
68 | scm_quiet(:pop, "-Rv #{stream}", "-L #{destination}", "'/./#{subdir}'") | |
69 | ] | |
70 | if subdir | |
71 | commands.push( | |
72 | "mv #{destination}/#{subdir}/* #{destination}", | |
73 | "rm -rf #{File.join(destination, subdir)}" | |
74 | ) | |
75 | end | |
76 | commands.join(' && ') | |
77 | end | |
78 | ||
79 | # Returns the command needed to show the changes that exist between the two revisions. | |
80 | def log(from, to=head) | |
81 | logger.info("Getting transactions between #{from} and #{to}") | |
82 | from_rev = InternalRevision.parse(from) | |
83 | to_rev = InternalRevision.parse(to) | |
84 | ||
85 | [ | |
86 | scm(:hist, '-s', from_rev.stream, '-t', "#{to_rev.transaction_id}-#{from_rev.transaction_id}"), | |
87 | "sed -e '/transaction #{from_rev.transaction_id}/ { Q }'" | |
88 | ].join(' | ') | |
89 | end | |
90 | ||
91 | # Returns the command needed to show the diff between what is deployed and what is | |
92 | # pending. Because Accurev can not do this task without creating some streams, | |
93 | # two time basis streams will be created for the purposes of doing the diff. | |
94 | def diff(from, to=head) | |
95 | from = InternalRevision.parse(from) | |
96 | to = InternalRevision.parse(to) | |
97 | ||
98 | from_stream = "#{from.stream}-capistrano-diff-from" | |
99 | to_stream = "#{to.stream}-capistrano-diff-to" | |
100 | ||
101 | [ | |
102 | change_or_create_stream(from_stream, from), | |
103 | change_or_create_stream(to_stream, to), | |
104 | scm(:diff, '-v', from_stream, '-V', to_stream, '-a') | |
105 | ].join(' && ') | |
106 | end | |
107 | ||
108 | private | |
109 | def depot | |
110 | repository.split('/')[0] | |
111 | end | |
112 | ||
113 | def stream | |
114 | variable(:stream) || depot | |
115 | end | |
116 | ||
117 | def subdir | |
118 | repository.split('/')[1..-1].join('/') unless repository.index('/').nil? | |
119 | end | |
120 | ||
121 | def change_or_create_stream(name, revision) | |
122 | [ | |
123 | scm_quiet(:mkstream, '-b', revision.stream, '-s', name, '-t', revision.transaction_id), | |
124 | scm_quiet(:chstream, '-b', revision.stream, '-s', name, '-t', revision.transaction_id) | |
125 | ].join('; ') | |
126 | end | |
127 | ||
128 | def show_streams_for(stream) | |
129 | scm :show, '-fx', '-s', stream, :streams | |
130 | end | |
131 | ||
132 | def scm_quiet(*args) | |
133 | scm(*args) + (variable(:scm_verbose) ? '' : '&> /dev/null') | |
134 | end | |
135 | ||
136 | class InternalRevision | |
137 | attr_reader :stream, :transaction_id | |
138 | ||
139 | def self.parse(string) | |
140 | match = /([^\/]+)(\/(.+)){0,1}/.match(string) | |
141 | raise "Unrecognized revision identifier: #{string}" unless match | |
142 | ||
143 | stream = match[1] | |
144 | transaction_id = match[3] || 'highest' | |
145 | InternalRevision.new(stream, transaction_id) | |
146 | end | |
147 | ||
148 | def initialize(stream, transaction_id) | |
149 | @stream = stream | |
150 | @transaction_id = transaction_id | |
151 | end | |
152 | ||
153 | def psuedo_revision? | |
154 | @transaction_id == 'highest' | |
155 | end | |
156 | ||
157 | def to_s | |
158 | "#{stream}/#{transaction_id}" | |
159 | end | |
160 | ||
161 | def ==(other) | |
162 | (stream == other.stream) && (transaction_id == other.transaction_id) | |
163 | end | |
164 | end | |
165 | end | |
166 | end | |
167 | end | |
168 | end |
0 | module Capistrano | |
1 | module Deploy | |
2 | module SCM | |
3 | ||
4 | # The ancestor class for all Capistrano SCM implementations. It provides | |
5 | # minimal infrastructure for subclasses to build upon and override. | |
6 | # | |
7 | # Note that subclasses that implement this abstract class only return | |
8 | # the commands that need to be executed--they do not execute the commands | |
9 | # themselves. In this way, the deployment method may execute the commands | |
10 | # either locally or remotely, as necessary. | |
11 | class Base | |
12 | class << self | |
13 | # If no parameters are given, it returns the current configured | |
14 | # name of the command-line utility of this SCM. If a parameter is | |
15 | # given, the defeault command is set to that value. | |
16 | def default_command(value=nil) | |
17 | if value | |
18 | @default_command = value | |
19 | else | |
20 | @default_command | |
21 | end | |
22 | end | |
23 | end | |
24 | ||
25 | # Wraps an SCM instance and forces all messages sent to it to be | |
26 | # relayed to the underlying SCM instance, in "local" mode. See | |
27 | # Base#local. | |
28 | class LocalProxy | |
29 | def initialize(scm) | |
30 | @scm = scm | |
31 | end | |
32 | ||
33 | def method_missing(sym, *args, &block) | |
34 | @scm.local { return @scm.send(sym, *args, &block) } | |
35 | end | |
36 | end | |
37 | ||
38 | # The options available for this SCM instance to reference. Should be | |
39 | # treated like a hash. | |
40 | attr_reader :configuration | |
41 | ||
42 | # Creates a new SCM instance with the given configuration options. | |
43 | def initialize(configuration={}) | |
44 | @configuration = configuration | |
45 | end | |
46 | ||
47 | # Returns a proxy that wraps the SCM instance and forces it to operate | |
48 | # in "local" mode, which changes how variables are looked up in the | |
49 | # configuration. Normally, if the value of a variable "foo" is needed, | |
50 | # it is queried for in the configuration as "foo". However, in "local" | |
51 | # mode, first "local_foo" would be looked for, and only if it is not | |
52 | # found would "foo" be used. This allows for both (e.g.) "scm_command" | |
53 | # and "local_scm_command" to be set, if the two differ. | |
54 | # | |
55 | # Alternatively, it may be called with a block, and for the duration of | |
56 | # the block, all requests on this configuration object will be | |
57 | # considered local. | |
58 | def local | |
59 | if block_given? | |
60 | begin | |
61 | saved, @local_mode = @local_mode, true | |
62 | yield | |
63 | ensure | |
64 | @local_mode = saved | |
65 | end | |
66 | else | |
67 | LocalProxy.new(self) | |
68 | end | |
69 | end | |
70 | ||
71 | # Returns true if running in "local" mode. See #local. | |
72 | def local? | |
73 | @local_mode | |
74 | end | |
75 | ||
76 | # Returns the string used to identify the latest revision in the | |
77 | # repository. This will be passed as the "revision" parameter of | |
78 | # the methods below. | |
79 | def head | |
80 | raise NotImplementedError, "`head' is not implemented by #{self.class.name}" | |
81 | end | |
82 | ||
83 | # Checkout a copy of the repository, at the given +revision+, to the | |
84 | # given +destination+. The checkout is suitable for doing development | |
85 | # work in, e.g. allowing subsequent commits and updates. | |
86 | def checkout(revision, destination) | |
87 | raise NotImplementedError, "`checkout' is not implemented by #{self.class.name}" | |
88 | end | |
89 | ||
90 | # Resynchronize the working copy in +destination+ to the specified | |
91 | # +revision+. | |
92 | def sync(revision, destination) | |
93 | raise NotImplementedError, "`sync' is not implemented by #{self.class.name}" | |
94 | end | |
95 | ||
96 | # Compute the difference between the two revisions, +from+ and +to+. | |
97 | def diff(from, to=nil) | |
98 | raise NotImplementedError, "`diff' is not implemented by #{self.class.name}" | |
99 | end | |
100 | ||
101 | # Return a log of all changes between the two specified revisions, | |
102 | # +from+ and +to+, inclusive. | |
103 | def log(from, to=nil) | |
104 | raise NotImplementedError, "`log' is not implemented by #{self.class.name}" | |
105 | end | |
106 | ||
107 | # If the given revision represents a "real" revision, this should | |
108 | # simply return the revision value. If it represends a pseudo-revision | |
109 | # (like Subversions "HEAD" identifier), it should yield a string | |
110 | # containing the commands that, when executed will return a string | |
111 | # that this method can then extract the real revision from. | |
112 | def query_revision(revision) | |
113 | raise NotImplementedError, "`query_revision' is not implemented by #{self.class.name}" | |
114 | end | |
115 | ||
116 | # Returns the revision number immediately following revision, if at | |
117 | # all possible. A block should always be passed to this method, which | |
118 | # accepts a command to invoke and returns the result, although a | |
119 | # particular SCM's implementation is not required to invoke the block. | |
120 | # | |
121 | # By default, this method simply returns the revision itself. If a | |
122 | # particular SCM is able to determine a subsequent revision given a | |
123 | # revision identifier, it should override this method. | |
124 | def next_revision(revision) | |
125 | revision | |
126 | end | |
127 | ||
128 | # Should analyze the given text and determine whether or not a | |
129 | # response is expected, and if so, return the appropriate response. | |
130 | # If no response is expected, return nil. The +state+ parameter is a | |
131 | # hash that may be used to preserve state between calls. This method | |
132 | # is used to define how Capistrano should respond to common prompts | |
133 | # and messages from the SCM, like password prompts and such. By | |
134 | # default, the output is simply displayed. | |
135 | def handle_data(state, stream, text) | |
136 | logger.info "[#{stream}] #{text}" | |
137 | nil | |
138 | end | |
139 | ||
140 | # Returns the name of the command-line utility for this SCM. It first | |
141 | # looks at the :scm_command variable, and if it does not exist, it | |
142 | # then falls back to whatever was defined by +default_command+. | |
143 | # | |
144 | # If scm_command is set to :default, the default_command will be | |
145 | # returned. | |
146 | def command | |
147 | command = variable(:scm_command) | |
148 | command = nil if command == :default | |
149 | command || default_command | |
150 | end | |
151 | ||
152 | # A helper method that can be used to define SCM commands naturally. | |
153 | # It returns a single string with all arguments joined by spaces, | |
154 | # with the scm command prefixed onto it. | |
155 | def scm(*args) | |
156 | [command, *args].compact.join(" ") | |
157 | end | |
158 | ||
159 | private | |
160 | ||
161 | # A helper for accessing variable values, which takes into | |
162 | # consideration the current mode ("normal" vs. "local"). | |
163 | def variable(name, default = nil) | |
164 | if local? && configuration.exists?("local_#{name}".to_sym) | |
165 | return configuration["local_#{name}".to_sym].nil? ? default : configuration["local_#{name}".to_sym] | |
166 | else | |
167 | configuration[name].nil? ? default : configuration[name] | |
168 | end | |
169 | end | |
170 | ||
171 | # A reference to a Logger instance that the SCM can use to log | |
172 | # activity. | |
173 | def logger | |
174 | @logger ||= variable(:logger) || Capistrano::Logger.new(:output => STDOUT) | |
175 | end | |
176 | ||
177 | # A helper for accessing the default command name for this SCM. It | |
178 | # simply delegates to the class' +default_command+ method. | |
179 | def default_command | |
180 | self.class.default_command | |
181 | end | |
182 | ||
183 | # A convenience method for accessing the declared repository value. | |
184 | def repository | |
185 | variable(:repository) | |
186 | end | |
187 | ||
188 | def arguments(command = :all) | |
189 | value = variable(:scm_arguments) | |
190 | if value.is_a?(Hash) | |
191 | value = value[command] | |
192 | end | |
193 | value | |
194 | end | |
195 | end | |
196 | ||
197 | end | |
198 | end | |
199 | end |
0 | require 'capistrano/recipes/deploy/scm/base' | |
1 | ||
2 | module Capistrano | |
3 | module Deploy | |
4 | module SCM | |
5 | ||
6 | # Implements the Capistrano SCM interface for the Bazaar-NG revision | |
7 | # control system (http://bazaar-vcs.org/). | |
8 | class Bzr < Base | |
9 | # Sets the default command name for this SCM. Users may override this | |
10 | # by setting the :scm_command variable. | |
11 | default_command "bzr" | |
12 | ||
13 | # Bazaar-NG doesn't support any pseudo-id's, so we'll use the convention | |
14 | # in this adapter that the :head symbol means the most recently | |
15 | # committed revision. | |
16 | def head | |
17 | :head | |
18 | end | |
19 | ||
20 | # Returns the command that will check out the given revision to the | |
21 | # given destination. | |
22 | def checkout(revision, destination) | |
23 | scm :checkout, "--lightweight", revswitch(revision), repository, destination | |
24 | end | |
25 | ||
26 | # The bzr 'update' command does not support updating to a specific | |
27 | # revision, so this just does update, followed by revert (unless | |
28 | # updating to head). | |
29 | def sync(revision, destination) | |
30 | commands = [scm(:update, destination)] | |
31 | commands << [scm(:revert, revswitch(revision), destination)] if revision != head | |
32 | commands.join(" && ") | |
33 | end | |
34 | ||
35 | # The bzr 'export' does an export similar to other SCM systems | |
36 | def export(revision, destination) | |
37 | scm :export, revswitch(revision), destination, repository | |
38 | end | |
39 | ||
40 | # The bzr "diff" command doesn't accept a repository argument, so it | |
41 | # must be run from within a working tree. | |
42 | def diff(from, to=nil) | |
43 | switch = "-r#{from}" | |
44 | switch << "..#{to}" if to | |
45 | ||
46 | scm :diff, switch | |
47 | end | |
48 | ||
49 | # Returns a log of changes between the two revisions (inclusive). | |
50 | def log(from, to=nil) | |
51 | scm :log, "--short", "-r#{from}..#{to}", repository | |
52 | end | |
53 | ||
54 | # Attempts to translate the given revision identifier to a "real" | |
55 | # revision. If the identifier is :head, the "bzr revno" command will | |
56 | # be yielded, and the block must execute the command and return the | |
57 | # output. The revision will be extracted from the output and returned. | |
58 | # If the 'revision' argument, on the other hand, is not :head, it is | |
59 | # simply returned. | |
60 | def query_revision(revision) | |
61 | return revision unless :head == revision | |
62 | ||
63 | command = scm('revno', repository) | |
64 | result = yield(command) | |
65 | end | |
66 | ||
67 | # Increments the given revision number and returns it. | |
68 | def next_revision(revision) | |
69 | revision.to_i + 1 | |
70 | end | |
71 | ||
72 | private | |
73 | ||
74 | def revswitch(revision) | |
75 | if revision == :head || revision.nil? | |
76 | nil | |
77 | else | |
78 | "-r #{revision}".chomp | |
79 | end | |
80 | end | |
81 | end | |
82 | ||
83 | end | |
84 | end | |
85 | end |
0 | require 'capistrano/recipes/deploy/scm/base' | |
1 | ||
2 | module Capistrano | |
3 | module Deploy | |
4 | module SCM | |
5 | ||
6 | # Implements the Capistrano SCM interface for the CVS revision | |
7 | # control system. | |
8 | class Cvs < Base | |
9 | # Sets the default command name for this SCM. Users may override this | |
10 | # by setting the :scm_command variable. | |
11 | default_command "cvs" | |
12 | ||
13 | # CVS understands 'HEAD' to refer to the latest revision in the | |
14 | # repository. | |
15 | def head | |
16 | "HEAD" | |
17 | end | |
18 | ||
19 | # Returns the command that will check out the given revision to the | |
20 | # given destination. | |
21 | def checkout(revision, destination) | |
22 | [ prep_destination(destination), | |
23 | scm(verbose, cvs_root, :checkout, cvs_revision(revision), cvs_destination(destination), variable(:scm_module)) | |
24 | ].join(' && ') | |
25 | end | |
26 | ||
27 | # Returns the command that will do an "cvs update" to the given | |
28 | # revision, for the working copy at the given destination. | |
29 | def sync(revision, destination) | |
30 | [ prep_destination(destination), | |
31 | scm(verbose, cvs_root, :update, cvs_revision(revision), cvs_destination(destination)) | |
32 | ].join(' && ') | |
33 | end | |
34 | ||
35 | # Returns the command that will do an "cvs export" of the given revision | |
36 | # to the given destination. | |
37 | def export(revision, destination) | |
38 | [ prep_destination(destination), | |
39 | scm(verbose, cvs_root, :export, cvs_revision(revision), cvs_destination(destination), variable(:scm_module)) | |
40 | ].join(' && ') | |
41 | end | |
42 | ||
43 | # Returns the command that will do an "cvs diff" for the two revisions. | |
44 | def diff(from, to=nil) | |
45 | rev_type = revision_type(from) | |
46 | if rev_type == :date | |
47 | range_args = "-D '#{from}' -D '#{to || 'now'}'" | |
48 | else | |
49 | range_args = "-r '#{from}' -r '#{to || head}'" | |
50 | end | |
51 | scm cvs_root, :diff, range_args | |
52 | end | |
53 | ||
54 | # Returns an "cvs log" command for the two revisions. | |
55 | def log(from, to=nil) | |
56 | rev_type = revision_type(from) | |
57 | if rev_type == :date | |
58 | range_arg = "-d '#{from}<#{to || 'now'}'" | |
59 | else | |
60 | range_arg = "-r '#{from}:#{to || head}'" | |
61 | end | |
62 | scm cvs_root, :log, range_arg | |
63 | end | |
64 | ||
65 | # Unfortunately, cvs doesn't support the concept of a revision number like | |
66 | # subversion and other SCM's do. For now, we'll rely on getting the timestamp | |
67 | # of the latest checkin under the revision that's passed to us. | |
68 | def query_revision(revision) | |
69 | return revision if revision_type(revision) == :date | |
70 | revision = yield(scm(cvs_root, :log, "-r#{revision}")). | |
71 | split("\n"). | |
72 | select { |line| line =~ /^date:/ }. | |
73 | map { |line| line[/^date: (.*?);/, 1] }. | |
74 | sort.last + " UTC" | |
75 | return revision | |
76 | end | |
77 | ||
78 | # Determines what the response should be for a particular bit of text | |
79 | # from the SCM. Password prompts, connection requests, passphrases, | |
80 | # etc. are handled here. | |
81 | def handle_data(state, stream, text) | |
82 | logger.info "[#{stream}] #{text}" | |
83 | case text | |
84 | when /\bpassword.*:/i | |
85 | # prompting for a password | |
86 | "#{variable(:scm_password) || variable(:password)}\n" | |
87 | when %r{\(yes/no\)} | |
88 | # let's be agreeable... | |
89 | "yes\n" | |
90 | end | |
91 | end | |
92 | ||
93 | private | |
94 | ||
95 | # Constructs the CVSROOT command-line option | |
96 | def cvs_root | |
97 | root = "" | |
98 | root << "-d #{repository} " if repository | |
99 | root | |
100 | end | |
101 | ||
102 | # Constructs the destination dir command-line option | |
103 | def cvs_destination(destination) | |
104 | dest = "" | |
105 | if destination | |
106 | dest_parts = destination.split(/\//); | |
107 | dest << "-d #{dest_parts.pop}" | |
108 | end | |
109 | dest | |
110 | end | |
111 | ||
112 | # attempts to guess what type of revision we're working with | |
113 | def revision_type(rev) | |
114 | return :date if rev =~ /^\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2} UTC$/ # i.e 2007-05-15 08:13:25 UTC | |
115 | return :date if rev =~ /^\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}$/ # i.e 2007-05-15 08:13:25 | |
116 | return :revision if rev =~ /^\d/ # i.e. 1.2.1 | |
117 | return :tag # i.e. RELEASE_1_2 | |
118 | end | |
119 | ||
120 | # constructs the appropriate command-line switch for specifying a | |
121 | # "revision" in CVS. This could be a tag, branch, revision (i.e. 1.3) | |
122 | # or a date (to be used with -d) | |
123 | def cvs_revision(rev) | |
124 | revision = "" | |
125 | revision << case revision_type(rev) | |
126 | when :date | |
127 | "-D \"#{rev}\"" if revision_type(rev) == :date | |
128 | when :revision | |
129 | "-r #{rev}" | |
130 | else | |
131 | "-r #{head}" | |
132 | end | |
133 | return revision | |
134 | end | |
135 | ||
136 | # If verbose output is requested, return nil, otherwise return the | |
137 | # command-line switch for "quiet" ("-Q"). | |
138 | def verbose | |
139 | variable(:scm_verbose) ? nil : "-Q" | |
140 | end | |
141 | ||
142 | def prep_destination(destination) | |
143 | dest_parts = destination.split(/\//); | |
144 | checkout_dir = dest_parts.pop | |
145 | dest = dest_parts.join('/') | |
146 | "mkdir -p #{ dest } && cd #{ dest }" | |
147 | end | |
148 | end | |
149 | ||
150 | end | |
151 | end | |
152 | end |
0 | require 'capistrano/recipes/deploy/scm/base' | |
1 | ||
2 | module Capistrano | |
3 | module Deploy | |
4 | module SCM | |
5 | ||
6 | # Implements the Capistrano SCM interface for the darcs revision | |
7 | # control system (http://www.abridgegame.org/darcs/). | |
8 | class Darcs < Base | |
9 | # Sets the default command name for this SCM. Users may override this | |
10 | # by setting the :scm_command variable. | |
11 | default_command "darcs" | |
12 | ||
13 | # Because darcs does not have any support for pseudo-ids, we'll just | |
14 | # return something here that we can use in the helpers below for | |
15 | # determining whether we need to look up the latest revision. | |
16 | def head | |
17 | :head | |
18 | end | |
19 | ||
20 | def to_match(revision) | |
21 | if revision.nil? || revision == self.head | |
22 | nil | |
23 | else | |
24 | "--to-match='hash #{revision}'" | |
25 | end | |
26 | end | |
27 | ||
28 | # Returns the command that will check out the given revision to the | |
29 | # given destination. The 'revision' parameter must be the 'hash' value | |
30 | # for the revision in question, as given by 'darcs changes --xml-output'. | |
31 | def checkout(revision, destination) | |
32 | scm :get, *[verbose, | |
33 | "--repo-name=#{destination}", | |
34 | to_match(revision), | |
35 | repository].compact | |
36 | end | |
37 | ||
38 | # Tries to update the destination repository in-place, to bring it up | |
39 | # to the given revision. Note that because darcs' "pull" operation | |
40 | # does not support a "to-match" argument (or similar), this basically | |
41 | # nukes the destination directory and re-gets it. | |
42 | def sync(revision, destination) | |
43 | ["rm -rf #{destination}", checkout(revision, destination)].join(" && ") | |
44 | end | |
45 | ||
46 | # Darcs does not have a real 'export' option; there is 'darcs dist', | |
47 | # but that presupposes a utility that can untar and ungzip the dist | |
48 | # file. We'll cheat and just do a get, followed by a deletion of the | |
49 | # _darcs metadata directory. | |
50 | def export(revision, destination) | |
51 | [checkout(revision, destination), "rm -rf #{destination}/_darcs"].join(" && ") | |
52 | end | |
53 | ||
54 | # Returns the command that will do a "darcs diff" for the two revisions. | |
55 | # Each revision must be the 'hash' identifier of a darcs revision. | |
56 | def diff(from, to=nil) | |
57 | scm :diff, "--from-match 'hash #{from}'", to && "--to-match 'hash #{to}'" | |
58 | end | |
59 | ||
60 | # Returns the log of changes between the two revisions. Each revision | |
61 | # must be the 'hash' identifier of a darcs revision. | |
62 | def log(from, to=nil) | |
63 | scm :changes, "--from-match 'hash #{from}'", to && "--to-match 'hash #{to}'", "--repo=#{repository}" | |
64 | end | |
65 | ||
66 | # Attempts to translate the given revision identifier to a "real" | |
67 | # revision. If the identifier is a symbol, it is assumed to be a | |
68 | # pseudo-id. Otherwise, it will be immediately returned. If it is a | |
69 | # pseudo-id, a set of commands to execute will be yielded, and the | |
70 | # result of executing those commands must be returned by the block. | |
71 | # This method will then extract the actual revision hash from the | |
72 | # returned data. | |
73 | def query_revision(revision) | |
74 | case revision | |
75 | when :head | |
76 | xml = yield(scm(:changes, "--last 1", "--xml-output", "--repo=#{repository}")) | |
77 | return xml[/hash='(.*?)'/, 1] | |
78 | else return revision | |
79 | end | |
80 | end | |
81 | ||
82 | private | |
83 | ||
84 | def verbose | |
85 | case variable(:scm_verbose) | |
86 | when nil then "-q" | |
87 | when false then nil | |
88 | else "-v" | |
89 | end | |
90 | end | |
91 | end | |
92 | ||
93 | end | |
94 | end | |
95 | end |
0 | require 'capistrano/recipes/deploy/scm/base' | |
1 | ||
2 | module Capistrano | |
3 | module Deploy | |
4 | module SCM | |
5 | ||
6 | # An SCM module for using Git as your source control tool with Capistrano | |
7 | # 2.0. If you are using Capistrano 1.x, use this plugin instead: | |
8 | # | |
9 | # http://scie.nti.st/2007/3/16/capistrano-with-git-shared-repository | |
10 | # | |
11 | # Assumes you are using a shared Git repository. | |
12 | # | |
13 | # Parts of this plugin borrowed from Scott Chacon's version, which I | |
14 | # found on the Capistrano mailing list but failed to be able to get | |
15 | # working. | |
16 | # | |
17 | # FEATURES: | |
18 | # | |
19 | # * Very simple, only requiring 2 lines in your deploy.rb. | |
20 | # * Can deploy different branches, tags, or any SHA1 easily. | |
21 | # * Supports prompting for password / passphrase upon checkout. | |
22 | # (I am amazed at how some plugins don't do this) | |
23 | # * Supports :scm_command, :scm_password, :scm_passphrase Capistrano | |
24 | # directives. | |
25 | # | |
26 | # CONFIGURATION | |
27 | # ------------- | |
28 | # | |
29 | # Use this plugin by adding the following line in your config/deploy.rb: | |
30 | # | |
31 | # set :scm, :git | |
32 | # | |
33 | # Set <tt>:repository</tt> to the path of your Git repo: | |
34 | # | |
35 | # set :repository, "someuser@somehost:/home/myproject" | |
36 | # | |
37 | # The above two options are required to be set, the ones below are | |
38 | # optional. | |
39 | # | |
40 | # You may set <tt>:branch</tt>, which is the reference to the branch, tag, | |
41 | # or any SHA1 you are deploying, for example: | |
42 | # | |
43 | # set :branch, "master" | |
44 | # | |
45 | # Otherwise, HEAD is assumed. I strongly suggest you set this. HEAD is | |
46 | # not always the best assumption. | |
47 | # | |
48 | # You may also set <tt>:remote</tt>, which will be used as a name for remote | |
49 | # tracking of repositories. This option is intended for use with the | |
50 | # <tt>:remote_cache</tt> strategy in a distributed git environment. | |
51 | # | |
52 | # For example in the projects <tt>config/deploy.rb</tt>: | |
53 | # | |
54 | # set :repository, "#{scm_user}@somehost:~/projects/project.git" | |
55 | # set :remote, "#{scm_user}" | |
56 | # | |
57 | # Then each person with deploy priveledges can add the following to their | |
58 | # local <tt>~/.caprc</tt> file: | |
59 | # | |
60 | # set :scm_user, 'someuser' | |
61 | # | |
62 | # Now any time a person deploys the project, their repository will be | |
63 | # setup as a remote git repository within the cached repository. | |
64 | # | |
65 | # The <tt>:scm_command</tt> configuration variable, if specified, will | |
66 | # be used as the full path to the git executable on the *remote* machine: | |
67 | # | |
68 | # set :scm_command, "/opt/local/bin/git" | |
69 | # | |
70 | # For compatibility with deploy scripts that may have used the 1.x | |
71 | # version of this plugin before upgrading, <tt>:git</tt> is still | |
72 | # recognized as an alias for :scm_command. | |
73 | # | |
74 | # Set <tt>:scm_password</tt> to the password needed to clone your repo | |
75 | # if you don't have password-less (public key) entry: | |
76 | # | |
77 | # set :scm_password, "my_secret' | |
78 | # | |
79 | # Otherwise, you will be prompted for a password. | |
80 | # | |
81 | # <tt>:scm_passphrase</tt> is also supported. | |
82 | # | |
83 | # The remote cache strategy is also supported. | |
84 | # | |
85 | # set :repository_cache, "git_master" | |
86 | # set :deploy_via, :remote_cache | |
87 | # | |
88 | # For faster clone, you can also use shallow cloning. This will set the | |
89 | # '--depth' flag using the depth specified. This *cannot* be used | |
90 | # together with the :remote_cache strategy | |
91 | # | |
92 | # set :git_shallow_clone, 1 | |
93 | # | |
94 | # For those that don't like to leave your entire repository on | |
95 | # your production server you can: | |
96 | # | |
97 | # set :deploy_via, :export | |
98 | # | |
99 | # To deploy from a local repository: | |
100 | # | |
101 | # set :repository, "file://." | |
102 | # set :deploy_via, :copy | |
103 | # | |
104 | # AUTHORS | |
105 | # ------- | |
106 | # | |
107 | # Garry Dolley http://scie.nti.st | |
108 | # Contributions by Geoffrey Grosenbach http://topfunky.com | |
109 | # Scott Chacon http://jointheconversation.org | |
110 | # Alex Arnell http://twologic.com | |
111 | # and Phillip Goldenburg | |
112 | ||
113 | class Git < Base | |
114 | # Sets the default command name for this SCM on your *local* machine. | |
115 | # Users may override this by setting the :scm_command variable. | |
116 | default_command "git" | |
117 | ||
118 | # When referencing "head", use the branch we want to deploy or, by | |
119 | # default, Git's reference of HEAD (the latest changeset in the default | |
120 | # branch, usually called "master"). | |
121 | def head | |
122 | variable(:branch) || 'HEAD' | |
123 | end | |
124 | ||
125 | def origin | |
126 | variable(:remote) || 'origin' | |
127 | end | |
128 | ||
129 | # Performs a clone on the remote machine, then checkout on the branch | |
130 | # you want to deploy. | |
131 | def checkout(revision, destination) | |
132 | git = command | |
133 | remote = origin | |
134 | ||
135 | args = [] | |
136 | args << "-o #{remote}" unless remote == 'origin' | |
137 | if depth = variable(:git_shallow_clone) | |
138 | args << "--depth #{depth}" | |
139 | end | |
140 | ||
141 | execute = [] | |
142 | execute << "#{git} clone #{verbose} #{args.join(' ')} #{variable(:repository)} #{destination}" | |
143 | ||
144 | # checkout into a local branch rather than a detached HEAD | |
145 | execute << "cd #{destination} && #{git} checkout #{verbose} -b deploy #{revision}" | |
146 | ||
147 | if variable(:git_enable_submodules) | |
148 | execute << "#{git} submodule #{verbose} init" | |
149 | execute << "#{git} submodule #{verbose} sync" | |
150 | if false == variable(:git_submodules_recursive) | |
151 | execute << "#{git} submodule #{verbose} update --init" | |
152 | else | |
153 | execute << %Q(export GIT_RECURSIVE=$([ ! "`#{git} --version`" \\< "git version 1.6.5" ] && echo --recursive)) | |
154 | execute << "#{git} submodule #{verbose} update --init $GIT_RECURSIVE" | |
155 | end | |
156 | end | |
157 | ||
158 | execute.compact.join(" && ").gsub(/\s+/, ' ') | |
159 | end | |
160 | ||
161 | # An expensive export. Performs a checkout as above, then | |
162 | # removes the repo. | |
163 | def export(revision, destination) | |
164 | checkout(revision, destination) << " && rm -Rf #{destination}/.git" | |
165 | end | |
166 | ||
167 | # Merges the changes to 'head' since the last fetch, for remote_cache | |
168 | # deployment strategy | |
169 | def sync(revision, destination) | |
170 | git = command | |
171 | remote = origin | |
172 | ||
173 | execute = [] | |
174 | execute << "cd #{destination}" | |
175 | ||
176 | # Use git-config to setup a remote tracking branches. Could use | |
177 | # git-remote but it complains when a remote of the same name already | |
178 | # exists, git-config will just silenty overwrite the setting every | |
179 | # time. This could cause wierd-ness in the remote cache if the url | |
180 | # changes between calls, but as long as the repositories are all | |
181 | # based from each other it should still work fine. | |
182 | if remote != 'origin' | |
183 | execute << "#{git} config remote.#{remote}.url #{variable(:repository)}" | |
184 | execute << "#{git} config remote.#{remote}.fetch +refs/heads/*:refs/remotes/#{remote}/*" | |
185 | end | |
186 | ||
187 | # since we're in a local branch already, just reset to specified revision rather than merge | |
188 | execute << "#{git} fetch #{verbose} #{remote} && #{git} fetch --tags #{verbose} #{remote} && #{git} reset #{verbose} --hard #{revision}" | |
189 | ||
190 | if variable(:git_enable_submodules) | |
191 | execute << "#{git} submodule #{verbose} init" | |
192 | execute << "for mod in `#{git} submodule status | awk '{ print $2 }'`; do #{git} config -f .git/config submodule.${mod}.url `#{git} config -f .gitmodules --get submodule.${mod}.url` && echo Synced $mod; done" | |
193 | execute << "#{git} submodule #{verbose} sync" | |
194 | if false == variable(:git_submodules_recursive) | |
195 | execute << "#{git} submodule #{verbose} update --init" | |
196 | else | |
197 | execute << %Q(export GIT_RECURSIVE=$([ ! "`#{git} --version`" \\< "git version 1.6.5" ] && echo --recursive)) | |
198 | execute << "#{git} submodule #{verbose} update --init $GIT_RECURSIVE" | |
199 | end | |
200 | end | |
201 | ||
202 | # Make sure there's nothing else lying around in the repository (for | |
203 | # example, a submodule that has subsequently been removed). | |
204 | execute << "#{git} clean #{verbose} -d -x -f" | |
205 | ||
206 | execute.join(" && ") | |
207 | end | |
208 | ||
209 | # Returns a string of diffs between two revisions | |
210 | def diff(from, to=nil) | |
211 | return scm :diff, from unless to | |
212 | scm :diff, "#{from}..#{to}" | |
213 | end | |
214 | ||
215 | # Returns a log of changes between the two revisions (inclusive). | |
216 | def log(from, to=nil) | |
217 | scm :log, "#{from}..#{to}" | |
218 | end | |
219 | ||
220 | # Getting the actual commit id, in case we were passed a tag | |
221 | # or partial sha or something - it will return the sha if you pass a sha, too | |
222 | def query_revision(revision) | |
223 | raise ArgumentError, "Deploying remote branches is no longer supported. Specify the remote branch as a local branch for the git repository you're deploying from (ie: '#{revision.gsub('origin/', '')}' rather than '#{revision}')." if revision =~ /^origin\// | |
224 | return revision if revision =~ /^[0-9a-f]{40}$/ | |
225 | command = scm('ls-remote', repository, revision) | |
226 | result = yield(command) | |
227 | revdata = result.split(/[\t\n]/) | |
228 | newrev = nil | |
229 | revdata.each_slice(2) do |refs| | |
230 | rev, ref = *refs | |
231 | if ref.sub(/refs\/.*?\//, '').strip == revision.to_s | |
232 | newrev = rev | |
233 | break | |
234 | end | |
235 | end | |
236 | return newrev if newrev =~ /^[0-9a-f]{40}$/ | |
237 | ||
238 | # If sha is not found on remote, try expanding from local repository | |
239 | command = scm('rev-parse --revs-only', revision) | |
240 | newrev = yield(command).to_s.strip | |
241 | ||
242 | raise "Unable to resolve revision for '#{revision}' on repository '#{repository}'." unless newrev =~ /^[0-9a-f]{40}$/ | |
243 | return newrev | |
244 | end | |
245 | ||
246 | def command | |
247 | # For backwards compatibility with 1.x version of this module | |
248 | variable(:git) || super | |
249 | end | |
250 | ||
251 | # Determines what the response should be for a particular bit of text | |
252 | # from the SCM. Password prompts, connection requests, passphrases, | |
253 | # etc. are handled here. | |
254 | def handle_data(state, stream, text) | |
255 | host = state[:channel][:host] | |
256 | logger.info "[#{host} :: #{stream}] #{text}" | |
257 | case text | |
258 | when /\bpassword.*:/i | |
259 | # git is prompting for a password | |
260 | unless pass = variable(:scm_password) | |
261 | pass = Capistrano::CLI.password_prompt | |
262 | end | |
263 | "#{pass}\n" | |
264 | when %r{\(yes/no\)} | |
265 | # git is asking whether or not to connect | |
266 | "yes\n" | |
267 | when /passphrase/i | |
268 | # git is asking for the passphrase for the user's key | |
269 | unless pass = variable(:scm_passphrase) | |
270 | pass = Capistrano::CLI.password_prompt | |
271 | end | |
272 | "#{pass}\n" | |
273 | when /accept \(t\)emporarily/ | |
274 | # git is asking whether to accept the certificate | |
275 | "t\n" | |
276 | end | |
277 | end | |
278 | ||
279 | private | |
280 | ||
281 | # If verbose output is requested, return nil, otherwise return the | |
282 | # command-line switch for "quiet" ("-q"). | |
283 | def verbose | |
284 | variable(:scm_verbose) ? nil : "-q" | |
285 | end | |
286 | end | |
287 | end | |
288 | end | |
289 | end | |
290 |
0 | # Copyright 2007 Matthew Elder <sseses@gmail.com> | |
1 | # based on work by Tobias Luetke | |
2 | ||
3 | require 'capistrano/recipes/deploy/scm/base' | |
4 | ||
5 | module Capistrano | |
6 | module Deploy | |
7 | module SCM | |
8 | ||
9 | # Implements the Capistrano SCM interface for the Mercurial revision | |
10 | # control system (http://www.selenic.com/mercurial/). | |
11 | # Latest updates at http://tackletechnology.org/oss/cap2-mercurial | |
12 | class Mercurial < Base | |
13 | # Sets the default command name for this SCM. Users may override this | |
14 | # by setting the :scm_command variable. | |
15 | default_command "hg" | |
16 | ||
17 | # For mercurial HEAD == tip except that it bases this assumption on what | |
18 | # tip is in the current repository (so push before you deploy) | |
19 | def head | |
20 | variable(:branch) || "tip" | |
21 | end | |
22 | ||
23 | # Clone the repository and update to the specified changeset. | |
24 | def checkout(changeset, destination) | |
25 | clone(destination) + " && " + update(changeset, destination) | |
26 | end | |
27 | ||
28 | # Pull from the repository and update to the specified changeset. | |
29 | def sync(changeset, destination) | |
30 | pull(destination) + " && " + update(changeset, destination) | |
31 | end | |
32 | ||
33 | # One day we will have hg archive, although i think its not needed | |
34 | def export(revision, destination) | |
35 | raise NotImplementedError, "`diff' is not implemented by #{self.class.name}" + | |
36 | "use checkout strategy" | |
37 | end | |
38 | ||
39 | # Compute the difference between the two changesets +from+ and +to+ | |
40 | # as a unified diff. | |
41 | def diff(from, to=nil) | |
42 | scm :diff, | |
43 | "--rev #{from}", | |
44 | (to ? "--rev #{to}" : nil) | |
45 | end | |
46 | ||
47 | # Return a log of all changes between the two specified changesets, | |
48 | # +from+ and +to+, inclusive or the log for +from+ if +to+ is omitted. | |
49 | def log(from, to=nil) | |
50 | scm :log, | |
51 | verbose, | |
52 | "--rev #{from}" + | |
53 | (to ? ":#{to}" : "") | |
54 | end | |
55 | ||
56 | # Translates a tag to a changeset if needed or just returns changeset. | |
57 | def query_revision(changeset) | |
58 | cmd = scm :log, | |
59 | verbose, | |
60 | "-r #{changeset}", | |
61 | '--template "{node|short}"' | |
62 | yield cmd | |
63 | end | |
64 | ||
65 | # Determine response for SCM prompts | |
66 | # user/pass can come from ssh and http distribution methods | |
67 | # yes/no is for when ssh asks you about fingerprints | |
68 | def handle_data(state, stream, text) | |
69 | host = state[:channel][:host] | |
70 | logger.info "[#{host} :: #{stream}] #{text}" | |
71 | case text | |
72 | when /^user:/mi | |
73 | # support :scm_user for backwards compatibility of this module | |
74 | if user = variable(:scm_username) || variable(:scm_user) | |
75 | "#{user}\n" | |
76 | else | |
77 | raise "No variable :scm_username specified and Mercurial asked!\n" + | |
78 | "Prompt was: #{text}" | |
79 | end | |
80 | when /\bpassword:/mi | |
81 | unless pass = scm_password_or_prompt | |
82 | # fall back on old behavior of erroring out with msg | |
83 | raise "No variable :scm_password specified and Mercurial asked!\n" + | |
84 | "Prompt was: #{text}" | |
85 | end | |
86 | "#{pass}\n" | |
87 | when /yes\/no/i | |
88 | "yes\n" | |
89 | end | |
90 | end | |
91 | ||
92 | private | |
93 | ||
94 | # Fine grained mercurial commands | |
95 | def clone(destination) | |
96 | scm :clone, | |
97 | verbose, | |
98 | "--noupdate", # do not update to tip when cloning is done | |
99 | repository, # clone which repository? | |
100 | destination # and put the clone where? | |
101 | end | |
102 | ||
103 | def pull(destination) | |
104 | scm :pull, | |
105 | verbose, | |
106 | "--repository #{destination}", # pull changes into what? | |
107 | repository # and pull the changes from? | |
108 | end | |
109 | ||
110 | def update(changeset, destination) | |
111 | scm :update, | |
112 | verbose, | |
113 | "--repository #{destination}", # update what? | |
114 | "--clean", # ignore untracked changes | |
115 | changeset # update to this changeset | |
116 | end | |
117 | ||
118 | # verbosity configuration grokking :) | |
119 | def verbose | |
120 | case variable(:scm_verbose) | |
121 | when nil then nil | |
122 | when false then "--quiet" | |
123 | else "--verbose" | |
124 | end | |
125 | end | |
126 | ||
127 | # honor Cap 2.1+'s :scm_prefer_prompt if present | |
128 | def scm_password_or_prompt | |
129 | @scm_password_or_prompt ||= variable(:scm_password) || | |
130 | (Capistrano::CLI.password_prompt("hg password: ") if variable(:scm_prefer_prompt)) | |
131 | end | |
132 | ||
133 | end | |
134 | end | |
135 | end | |
136 | end |
0 | require 'capistrano/recipes/deploy/scm/base' | |
1 | ||
2 | module Capistrano | |
3 | module Deploy | |
4 | module SCM | |
5 | ||
6 | # A trivial SCM wrapper for representing the current working directory | |
7 | # as a repository. Obviously, not all operations are available for this | |
8 | # SCM, but it works sufficiently for use with the "copy" deployment | |
9 | # strategy. | |
10 | # | |
11 | # Use of this module is _not_ recommended; in general, it is good | |
12 | # practice to use some kind of source code management even for anything | |
13 | # you are wanting to deploy. However, this module is provided in | |
14 | # acknowledgement of the cases where trivial deployment of your current | |
15 | # working directory is desired. | |
16 | # | |
17 | # set :repository, "." | |
18 | # set :scm, :none | |
19 | # set :deploy_via, :copy | |
20 | class None < Base | |
21 | # No versioning, thus, no head. Returns the empty string. | |
22 | def head | |
23 | "" | |
24 | end | |
25 | ||
26 | # Simply does a copy from the :repository directory to the | |
27 | # :destination directory. | |
28 | def checkout(revision, destination) | |
29 | !Capistrano::Deploy::LocalDependency.on_windows? ? "cp -R #{repository} #{destination}" : "xcopy #{repository} \"#{destination}\" /S/I/Y/Q/E" | |
30 | end | |
31 | ||
32 | alias_method :export, :checkout | |
33 | ||
34 | # No versioning, so this just returns the argument, with no | |
35 | # modification. | |
36 | def query_revision(revision) | |
37 | revision | |
38 | end | |
39 | end | |
40 | ||
41 | end | |
42 | end | |
43 | end |
0 | require 'capistrano/recipes/deploy/scm/base' | |
1 | ||
2 | # Notes: | |
3 | # no global verbose flag for scm_verbose | |
4 | # sync, checkout and export are just sync in p4 | |
5 | # | |
6 | module Capistrano | |
7 | module Deploy | |
8 | module SCM | |
9 | ||
10 | # Implements the Capistrano SCM interface for the Perforce revision | |
11 | # control system (http://www.perforce.com). | |
12 | class Perforce < Base | |
13 | # Sets the default command name for this SCM. Users may override this | |
14 | # by setting the :scm_command variable. | |
15 | default_command "p4" | |
16 | ||
17 | # Perforce understands '#head' to refer to the latest revision in the | |
18 | # depot. | |
19 | def head | |
20 | 'head' | |
21 | end | |
22 | ||
23 | # Returns the command that will sync the given revision to the given | |
24 | # destination directory. The perforce client has a fixed destination so | |
25 | # the files must be copied from there to their intended resting place. | |
26 | def checkout(revision, destination) | |
27 | p4_sync(revision, destination, p4sync_flags) | |
28 | end | |
29 | ||
30 | # Returns the command that will sync the given revision to the given | |
31 | # destination directory. The perforce client has a fixed destination so | |
32 | # the files must be copied from there to their intended resting place. | |
33 | def sync(revision, destination) | |
34 | p4_sync(revision, destination, p4sync_flags) | |
35 | end | |
36 | ||
37 | # Returns the command that will sync the given revision to the given | |
38 | # destination directory. The perforce client has a fixed destination so | |
39 | # the files must be copied from there to their intended resting place. | |
40 | def export(revision, destination) | |
41 | p4_sync(revision, destination, p4sync_flags) | |
42 | end | |
43 | ||
44 | # Returns the command that will do an "p4 diff2" for the two revisions. | |
45 | def diff(from, to=head) | |
46 | scm authentication, :diff2, "-u -db", "//#{p4client}/...#{rev_no(from)}", "//#{p4client}/...#{rev_no(to)}" | |
47 | end | |
48 | ||
49 | # Returns a "p4 changes" command for the two revisions. | |
50 | def log(from=1, to=head) | |
51 | scm authentication, :changes, "-s submitted", "//#{p4client}/...#{rev_no(from)},#{rev_no(to)}" | |
52 | end | |
53 | ||
54 | def query_revision(revision) | |
55 | return revision if revision.to_s =~ /^\d+$/ | |
56 | command = scm(authentication, :changes, "-s submitted", "-m 1", "//#{p4client}/...#{rev_no(revision)}") | |
57 | yield(command)[/Change (\d+) on/, 1] | |
58 | end | |
59 | ||
60 | # Increments the given revision number and returns it. | |
61 | def next_revision(revision) | |
62 | revision.to_i + 1 | |
63 | end | |
64 | ||
65 | # Determines what the response should be for a particular bit of text | |
66 | # from the SCM. Password prompts, connection requests, passphrases, | |
67 | # etc. are handled here. | |
68 | def handle_data(state, stream, text) | |
69 | case text | |
70 | when /\(P4PASSWD\) invalid or unset\./i | |
71 | raise Capistrano::Error, "scm_password (or p4passwd) is incorrect or unset" | |
72 | when /Can.t create a new user.*/i | |
73 | raise Capistrano::Error, "scm_username (or p4user) is incorrect or unset" | |
74 | when /Perforce client error\:/i | |
75 | raise Capistrano::Error, "p4port is incorrect or unset" | |
76 | when /Client \'[\w\-\_\.]+\' unknown.*/i | |
77 | raise Capistrano::Error, "p4client is incorrect or unset" | |
78 | end | |
79 | end | |
80 | ||
81 | private | |
82 | ||
83 | # Builds the set of authentication switches that perforce understands. | |
84 | def authentication | |
85 | [ p4port && "-p #{p4port}", | |
86 | p4user && "-u #{p4user}", | |
87 | p4passwd && "-P #{p4passwd}", | |
88 | p4client && "-c #{p4client}" ].compact.join(" ") | |
89 | end | |
90 | ||
91 | # Returns the command that will sync the given revision to the given | |
92 | # destination directory with specific options. The perforce client has | |
93 | # a fixed destination so the files must be copied from there to their | |
94 | # intended resting place. | |
95 | def p4_sync(revision, destination, options="") | |
96 | scm authentication, :sync, options, "#{rev_no(revision)}", "&& cp -rf #{p4client_root} #{destination}" | |
97 | end | |
98 | ||
99 | def p4client | |
100 | variable(:p4client) | |
101 | end | |
102 | ||
103 | def p4port | |
104 | variable(:p4port) | |
105 | end | |
106 | ||
107 | def p4user | |
108 | variable(:p4user) || variable(:scm_username) | |
109 | end | |
110 | ||
111 | def p4passwd | |
112 | variable(:p4passwd) || variable(:scm_password) | |
113 | end | |
114 | ||
115 | def p4sync_flags | |
116 | variable(:p4sync_flags) || "-f" | |
117 | end | |
118 | ||
119 | def p4client_root | |
120 | variable(:p4client_root) || "`#{command} #{authentication} client -o | grep ^Root | cut -f2`" | |
121 | end | |
122 | ||
123 | def rev_no(revision) | |
124 | if variable(:p4_label) | |
125 | p4_label = if variable(:p4_label) =~ /\A@/ | |
126 | variable(:p4_label) | |
127 | else | |
128 | "@#{variable(:p4_label)}" | |
129 | end | |
130 | return p4_label | |
131 | end | |
132 | ||
133 | case revision.to_s | |
134 | when "head" | |
135 | "#head" | |
136 | when /^\d+/ | |
137 | "@#{revision}" | |
138 | else | |
139 | revision | |
140 | end | |
141 | end | |
142 | end | |
143 | ||
144 | end | |
145 | end | |
146 | end |
0 | require 'capistrano/recipes/deploy/scm/base' | |
1 | require 'yaml' | |
2 | ||
3 | module Capistrano | |
4 | module Deploy | |
5 | module SCM | |
6 | ||
7 | # Implements the Capistrano SCM interface for the Subversion revision | |
8 | # control system (http://subversion.tigris.org). | |
9 | class Subversion < Base | |
10 | # Sets the default command name for this SCM. Users may override this | |
11 | # by setting the :scm_command variable. | |
12 | default_command "svn" | |
13 | ||
14 | # Subversion understands 'HEAD' to refer to the latest revision in the | |
15 | # repository. | |
16 | def head | |
17 | "HEAD" | |
18 | end | |
19 | ||
20 | # Returns the command that will check out the given revision to the | |
21 | # given destination. | |
22 | def checkout(revision, destination) | |
23 | scm :checkout, arguments, arguments(:checkout), verbose, authentication, "-r#{revision}", repository, destination | |
24 | end | |
25 | ||
26 | # Returns the command that will do an "svn update" to the given | |
27 | # revision, for the working copy at the given destination. | |
28 | def sync(revision, destination) | |
29 | scm :switch, arguments, verbose, authentication, "-r#{revision}", repository, destination | |
30 | end | |
31 | ||
32 | # Returns the command that will do an "svn export" of the given revision | |
33 | # to the given destination. | |
34 | def export(revision, destination) | |
35 | scm :export, arguments, arguments(:export), verbose, authentication, "-r#{revision}", repository, destination | |
36 | end | |
37 | ||
38 | # Returns the command that will do an "svn diff" for the two revisions. | |
39 | def diff(from, to=nil) | |
40 | scm :diff, repository, arguments(:diff), authentication, "-r#{from}:#{to || head}" | |
41 | end | |
42 | ||
43 | # Returns an "svn log" command for the two revisions. | |
44 | def log(from, to=nil) | |
45 | scm :log, repository, arguments(:log), authentication, "-r#{from}:#{to || head}" | |
46 | end | |
47 | ||
48 | # Attempts to translate the given revision identifier to a "real" | |
49 | # revision. If the identifier is an integer, it will simply be returned. | |
50 | # Otherwise, this will yield a string of the commands it needs to be | |
51 | # executed (svn info), and will extract the revision from the response. | |
52 | def query_revision(revision) | |
53 | return revision if revision =~ /^\d+$/ | |
54 | command = scm(:info, arguments, arguments(:info), repository, authentication, "-r#{revision}") | |
55 | result = yield(command) | |
56 | yaml = YAML.load(result) | |
57 | raise "tried to run `#{command}' and got unexpected result #{result.inspect}" unless Hash === yaml | |
58 | [ (yaml['Last Changed Rev'] || 0).to_i, (yaml['Revision'] || 0).to_i ].max | |
59 | end | |
60 | ||
61 | # Increments the given revision number and returns it. | |
62 | def next_revision(revision) | |
63 | revision.to_i + 1 | |
64 | end | |
65 | ||
66 | # Determines what the response should be for a particular bit of text | |
67 | # from the SCM. Password prompts, connection requests, passphrases, | |
68 | # etc. are handled here. | |
69 | def handle_data(state, stream, text) | |
70 | host = state[:channel][:host] | |
71 | logger.info "[#{host} :: #{stream}] #{text}" | |
72 | case text | |
73 | when /\bpassword.*:/i | |
74 | # subversion is prompting for a password | |
75 | "#{scm_password_prompt}\n" | |
76 | when %r{\(yes/no\)} | |
77 | # subversion is asking whether or not to connect | |
78 | "yes\n" | |
79 | when /passphrase/i | |
80 | # subversion is asking for the passphrase for the user's key | |
81 | "#{variable(:scm_passphrase)}\n" | |
82 | when /The entry \'(.+?)\' is no longer a directory/ | |
83 | raise Capistrano::Error, "subversion can't update because directory '#{$1}' was replaced. Please add it to svn:ignore." | |
84 | when /accept \(t\)emporarily/ | |
85 | # subversion is asking whether to accept the certificate | |
86 | "t\n" | |
87 | end | |
88 | end | |
89 | ||
90 | private | |
91 | ||
92 | # If a username is configured for the SCM, return the command-line | |
93 | # switches for that. Note that we don't need to return the password | |
94 | # switch, since Capistrano will check for that prompt in the output | |
95 | # and will respond appropriately. | |
96 | def authentication | |
97 | username = variable(:scm_username) | |
98 | return "" unless username | |
99 | result = "--username #{variable(:scm_username)} " | |
100 | result << "--password #{variable(:scm_password)} " unless variable(:scm_auth_cache) || variable(:scm_prefer_prompt) | |
101 | result << "--no-auth-cache " unless variable(:scm_auth_cache) | |
102 | result | |
103 | end | |
104 | ||
105 | # If verbose output is requested, return nil, otherwise return the | |
106 | # command-line switch for "quiet" ("-q"). | |
107 | def verbose | |
108 | variable(:scm_verbose) ? nil : "-q" | |
109 | end | |
110 | ||
111 | def scm_password_prompt | |
112 | @scm_password_prompt ||= variable(:scm_password) || | |
113 | variable(:password) || | |
114 | Capistrano::CLI.password_prompt("Subversion password: ") | |
115 | end | |
116 | end | |
117 | ||
118 | end | |
119 | end | |
120 | end |
0 | module Capistrano | |
1 | module Deploy | |
2 | module SCM | |
3 | def self.new(scm, config={}) | |
4 | scm_file = "capistrano/recipes/deploy/scm/#{scm}" | |
5 | require(scm_file) | |
6 | ||
7 | scm_const = scm.to_s.capitalize.gsub(/_(.)/) { $1.upcase } | |
8 | if const_defined?(scm_const) | |
9 | const_get(scm_const).new(config) | |
10 | else | |
11 | raise Capistrano::Error, "could not find `#{name}::#{scm_const}' in `#{scm_file}'" | |
12 | end | |
13 | rescue LoadError | |
14 | raise Capistrano::Error, "could not find any SCM named `#{scm}'" | |
15 | end | |
16 | end | |
17 | end | |
18 | end⏎ |
0 | require 'benchmark' | |
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 | result = nil | |
53 | if RUBY_PLATFORM =~ /win32/ | |
54 | cmd = cmd.split(/\s+/).collect {|w| w.match(/^[\w+]+:\/\//) ? w : w.gsub('/', '\\') }.join(' ') # Split command by spaces, change / by \\ unless element is a some+thing:// | |
55 | cmd.gsub!(/^cd /,'cd /D ') # Replace cd with cd /D | |
56 | cmd.gsub!(/&& cd /,'&& cd /D ') # Replace cd with cd /D | |
57 | logger.trace "executing locally: #{cmd}" | |
58 | elapsed = Benchmark.realtime do | |
59 | result = super(cmd) | |
60 | end | |
61 | else | |
62 | logger.trace "executing locally: #{cmd}" | |
63 | elapsed = Benchmark.realtime do | |
64 | result = super | |
65 | end | |
66 | end | |
67 | ||
68 | logger.trace "command finished in #{(elapsed * 1000).round}ms" | |
69 | result | |
70 | end | |
71 | ||
72 | private | |
73 | ||
74 | def logger | |
75 | @logger ||= configuration[:logger] || Capistrano::Logger.new(:output => STDOUT) | |
76 | end | |
77 | ||
78 | # The revision to deploy. Must return a real revision identifier, | |
79 | # and not a pseudo-id. | |
80 | def revision | |
81 | configuration[:real_revision] | |
82 | end | |
83 | end | |
84 | ||
85 | end | |
86 | end | |
87 | end |
0 | require 'capistrano/recipes/deploy/strategy/remote' | |
1 | ||
2 | module Capistrano | |
3 | module Deploy | |
4 | module Strategy | |
5 | ||
6 | # Implements the deployment strategy which does an SCM checkout on each | |
7 | # target host. This is the default deployment strategy for Capistrano. | |
8 | class Checkout < Remote | |
9 | protected | |
10 | ||
11 | # Returns the SCM's checkout command for the revision to deploy. | |
12 | def command | |
13 | @command ||= source.checkout(revision, configuration[:release_path]) | |
14 | end | |
15 | end | |
16 | ||
17 | end | |
18 | end | |
19 | end |
0 | require 'capistrano/recipes/deploy/strategy/base' | |
1 | require 'fileutils' | |
2 | require 'tempfile' # Dir.tmpdir | |
3 | ||
4 | module Capistrano | |
5 | module Deploy | |
6 | module Strategy | |
7 | ||
8 | # This class implements the strategy for deployments which work | |
9 | # by preparing the source code locally, compressing it, copying the | |
10 | # file to each target host, and uncompressing it to the deployment | |
11 | # directory. | |
12 | # | |
13 | # By default, the SCM checkout command is used to obtain the local copy | |
14 | # of the source code. If you would rather use the export operation, | |
15 | # you can set the :copy_strategy variable to :export. | |
16 | # | |
17 | # set :copy_strategy, :export | |
18 | # | |
19 | # For even faster deployments, you can set the :copy_cache variable to | |
20 | # true. This will cause deployments to do a new checkout of your | |
21 | # repository to a new directory, and then copy that checkout. Subsequent | |
22 | # deploys will just resync that copy, rather than doing an entirely new | |
23 | # checkout. Additionally, you can specify file patterns to exclude from | |
24 | # the copy when using :copy_cache; just set the :copy_exclude variable | |
25 | # to a file glob (or an array of globs). | |
26 | # | |
27 | # set :copy_cache, true | |
28 | # set :copy_exclude, ".git/*" | |
29 | # | |
30 | # Note that :copy_strategy is ignored when :copy_cache is set. Also, if | |
31 | # you want the copy cache put somewhere specific, you can set the variable | |
32 | # to the path you want, instead of merely 'true': | |
33 | # | |
34 | # set :copy_cache, "/tmp/caches/myapp" | |
35 | # | |
36 | # This deployment strategy also supports a special variable, | |
37 | # :copy_compression, which must be one of :gzip, :bz2, or | |
38 | # :zip, and which specifies how the source should be compressed for | |
39 | # transmission to each host. | |
40 | # | |
41 | # There is a possibility to pass a build command that will get | |
42 | # executed if your code needs to be compiled or something needs to be | |
43 | # done before the code is ready to run. | |
44 | # | |
45 | # set :build_script, "make all" | |
46 | # | |
47 | # Note that if you use :copy_cache, the :build_script is used on the | |
48 | # cache and thus you get faster compilation if your script does not | |
49 | # recompile everything. | |
50 | class Copy < Base | |
51 | # Obtains a copy of the source code locally (via the #command method), | |
52 | # compresses it to a single file, copies that file to all target | |
53 | # servers, and uncompresses it on each of them into the deployment | |
54 | # directory. | |
55 | def deploy! | |
56 | copy_cache ? run_copy_cache_strategy : run_copy_strategy | |
57 | ||
58 | create_revision_file | |
59 | compress_repository | |
60 | distribute! | |
61 | ensure | |
62 | rollback_changes | |
63 | end | |
64 | ||
65 | def build directory | |
66 | execute "running build script on #{directory}" do | |
67 | Dir.chdir(directory) { system(build_script) } | |
68 | end if build_script | |
69 | end | |
70 | ||
71 | def check! | |
72 | super.check do |d| | |
73 | d.local.command(source.local.command) if source.local.command | |
74 | d.local.command(compress(nil, nil).first) | |
75 | d.remote.command(decompress(nil).first) | |
76 | end | |
77 | end | |
78 | ||
79 | # Returns the location of the local copy cache, if the strategy should | |
80 | # use a local cache + copy instead of a new checkout/export every | |
81 | # time. Returns +nil+ unless :copy_cache has been set. If :copy_cache | |
82 | # is +true+, a default cache location will be returned. | |
83 | def copy_cache | |
84 | @copy_cache ||= configuration[:copy_cache] == true ? | |
85 | File.expand_path(configuration[:application], Dir.tmpdir) : | |
86 | File.expand_path(configuration[:copy_cache], Dir.pwd) rescue nil | |
87 | end | |
88 | ||
89 | private | |
90 | ||
91 | def run_copy_cache_strategy | |
92 | copy_repository_to_local_cache | |
93 | build copy_cache | |
94 | copy_cache_to_staging_area | |
95 | end | |
96 | ||
97 | def run_copy_strategy | |
98 | copy_repository_to_server | |
99 | build destination | |
100 | remove_excluded_files if copy_exclude.any? | |
101 | end | |
102 | ||
103 | def execute description, &block | |
104 | logger.debug description | |
105 | handle_system_errors &block | |
106 | end | |
107 | ||
108 | def handle_system_errors &block | |
109 | block.call | |
110 | raise_command_failed if last_command_failed? | |
111 | end | |
112 | ||
113 | def refresh_local_cache | |
114 | execute "refreshing local cache to revision #{revision} at #{copy_cache}" do | |
115 | system(source.sync(revision, copy_cache)) | |
116 | end | |
117 | end | |
118 | ||
119 | def create_local_cache | |
120 | execute "preparing local cache at #{copy_cache}" do | |
121 | system(source.checkout(revision, copy_cache)) | |
122 | end | |
123 | end | |
124 | ||
125 | def raise_command_failed | |
126 | raise Capistrano::Error, "shell command failed with return code #{$?}" | |
127 | end | |
128 | ||
129 | def last_command_failed? | |
130 | $? != 0 | |
131 | end | |
132 | ||
133 | def copy_cache_to_staging_area | |
134 | execute "copying cache to deployment staging area #{destination}" do | |
135 | create_destination | |
136 | Dir.chdir(copy_cache) { copy_files(queue_files) } | |
137 | end | |
138 | end | |
139 | ||
140 | def create_destination | |
141 | FileUtils.mkdir_p(destination) | |
142 | end | |
143 | ||
144 | def copy_files files | |
145 | files.each { |name| process_file(name) } | |
146 | end | |
147 | ||
148 | def process_file name | |
149 | send "copy_#{filetype(name)}", name | |
150 | end | |
151 | ||
152 | def filetype name | |
153 | filetype = File.ftype name | |
154 | filetype = "file" unless ["link", "directory"].include? filetype | |
155 | filetype | |
156 | end | |
157 | ||
158 | def copy_link name | |
159 | FileUtils.ln_s(File.readlink(name), File.join(destination, name)) | |
160 | end | |
161 | ||
162 | def copy_directory name | |
163 | FileUtils.mkdir(File.join(destination, name)) | |
164 | copy_files(queue_files(name)) | |
165 | end | |
166 | ||
167 | def copy_file name | |
168 | FileUtils.ln(name, File.join(destination, name)) | |
169 | end | |
170 | ||
171 | def queue_files directory=nil | |
172 | Dir.glob(pattern_for(directory), File::FNM_DOTMATCH).reject! { |file| excluded_files_contain? file } | |
173 | end | |
174 | ||
175 | def pattern_for directory | |
176 | !directory.nil? ? "#{directory}/*" : "*" | |
177 | end | |
178 | ||
179 | def excluded_files_contain? file | |
180 | copy_exclude.any? { |p| File.fnmatch(p, file) } or [ ".", ".."].include? File.basename(file) | |
181 | end | |
182 | ||
183 | def copy_repository_to_server | |
184 | execute "getting (via #{copy_strategy}) revision #{revision} to #{destination}" do | |
185 | copy_repository_via_strategy | |
186 | end | |
187 | end | |
188 | ||
189 | def copy_repository_via_strategy | |
190 | system(command) | |
191 | end | |
192 | ||
193 | def remove_excluded_files | |
194 | logger.debug "processing exclusions..." | |
195 | ||
196 | copy_exclude.each do |pattern| | |
197 | delete_list = Dir.glob(File.join(destination, pattern), File::FNM_DOTMATCH) | |
198 | # avoid the /.. trap that deletes the parent directories | |
199 | delete_list.delete_if { |dir| dir =~ /\/\.\.$/ } | |
200 | FileUtils.rm_rf(delete_list.compact) | |
201 | end | |
202 | end | |
203 | ||
204 | def create_revision_file | |
205 | File.open(File.join(destination, "REVISION"), "w") { |f| f.puts(revision) } | |
206 | end | |
207 | ||
208 | def compress_repository | |
209 | execute "Compressing #{destination} to #{filename}" do | |
210 | Dir.chdir(copy_dir) { system(compress(File.basename(destination), File.basename(filename)).join(" ")) } | |
211 | end | |
212 | end | |
213 | ||
214 | def rollback_changes | |
215 | FileUtils.rm filename rescue nil | |
216 | FileUtils.rm_rf destination rescue nil | |
217 | end | |
218 | ||
219 | def copy_repository_to_local_cache | |
220 | return refresh_local_cache if File.exists?(copy_cache) | |
221 | create_local_cache | |
222 | end | |
223 | ||
224 | def build_script | |
225 | configuration[:build_script] | |
226 | end | |
227 | ||
228 | # Specify patterns to exclude from the copy. This is only valid | |
229 | # when using a local cache. | |
230 | def copy_exclude | |
231 | @copy_exclude ||= Array(configuration.fetch(:copy_exclude, [])) | |
232 | end | |
233 | ||
234 | # Returns the basename of the release_path, which will be used to | |
235 | # name the local copy and archive file. | |
236 | def destination | |
237 | @destination ||= File.join(copy_dir, File.basename(configuration[:release_path])) | |
238 | end | |
239 | ||
240 | # Returns the value of the :copy_strategy variable, defaulting to | |
241 | # :checkout if it has not been set. | |
242 | def copy_strategy | |
243 | @copy_strategy ||= configuration.fetch(:copy_strategy, :checkout) | |
244 | end | |
245 | ||
246 | # Should return the command(s) necessary to obtain the source code | |
247 | # locally. | |
248 | def command | |
249 | @command ||= case copy_strategy | |
250 | when :checkout | |
251 | source.checkout(revision, destination) | |
252 | when :export | |
253 | source.export(revision, destination) | |
254 | end | |
255 | end | |
256 | ||
257 | # Returns the name of the file that the source code will be | |
258 | # compressed to. | |
259 | def filename | |
260 | @filename ||= File.join(copy_dir, "#{File.basename(destination)}.#{compression.extension}") | |
261 | end | |
262 | ||
263 | # The directory to which the copy should be checked out | |
264 | def copy_dir | |
265 | @copy_dir ||= File.expand_path(configuration[:copy_dir] || Dir.tmpdir, Dir.pwd) | |
266 | end | |
267 | ||
268 | # The directory on the remote server to which the archive should be | |
269 | # copied | |
270 | def remote_dir | |
271 | @remote_dir ||= configuration[:copy_remote_dir] || "/tmp" | |
272 | end | |
273 | ||
274 | # The location on the remote server where the file should be | |
275 | # temporarily stored. | |
276 | def remote_filename | |
277 | @remote_filename ||= File.join(remote_dir, File.basename(filename)) | |
278 | end | |
279 | ||
280 | # A struct for representing the specifics of a compression type. | |
281 | # Commands are arrays, where the first element is the utility to be | |
282 | # used to perform the compression or decompression. | |
283 | Compression = Struct.new(:extension, :compress_command, :decompress_command) | |
284 | ||
285 | # The compression method to use, defaults to :gzip. | |
286 | def compression | |
287 | remote_tar = configuration[:copy_remote_tar] || 'tar' | |
288 | local_tar = configuration[:copy_local_tar] || 'tar' | |
289 | ||
290 | type = configuration[:copy_compression] || :gzip | |
291 | case type | |
292 | when :gzip, :gz then Compression.new("tar.gz", [local_tar, 'czf'], [remote_tar, 'xzf']) | |
293 | when :bzip2, :bz2 then Compression.new("tar.bz2", [local_tar, 'cjf'], [remote_tar, 'xjf']) | |
294 | when :zip then Compression.new("zip", %w(zip -qyr), %w(unzip -q)) | |
295 | else raise ArgumentError, "invalid compression type #{type.inspect}" | |
296 | end | |
297 | end | |
298 | ||
299 | # Returns the command necessary to compress the given directory | |
300 | # into the given file. | |
301 | def compress(directory, file) | |
302 | compression.compress_command + [file, directory] | |
303 | end | |
304 | ||
305 | # Returns the command necessary to decompress the given file, | |
306 | # relative to the current working directory. It must also | |
307 | # preserve the directory structure in the file. | |
308 | def decompress(file) | |
309 | compression.decompress_command + [file] | |
310 | end | |
311 | ||
312 | def decompress_remote_file | |
313 | run "cd #{configuration[:releases_path]} && #{decompress(remote_filename).join(" ")} && rm #{remote_filename}" | |
314 | end | |
315 | ||
316 | # Distributes the file to the remote servers | |
317 | def distribute! | |
318 | upload(filename, remote_filename) | |
319 | decompress_remote_file | |
320 | end | |
321 | end | |
322 | ||
323 | end | |
324 | end | |
325 | end |
0 | require 'capistrano/recipes/deploy/strategy/remote' | |
1 | ||
2 | module Capistrano | |
3 | module Deploy | |
4 | module Strategy | |
5 | ||
6 | # Implements the deployment strategy which does an SCM export on each | |
7 | # target host. | |
8 | class Export < Remote | |
9 | protected | |
10 | ||
11 | # Returns the SCM's export command for the revision to deploy. | |
12 | def command | |
13 | @command ||= source.export(revision, configuration[:release_path]) | |
14 | end | |
15 | end | |
16 | ||
17 | end | |
18 | end | |
19 | end |
0 | require 'capistrano/recipes/deploy/strategy/base' | |
1 | ||
2 | module Capistrano | |
3 | module Deploy | |
4 | module Strategy | |
5 | ||
6 | # An abstract superclass, which forms the base for all deployment | |
7 | # strategies which work by grabbing the code from the repository directly | |
8 | # from remote host. This includes deploying by checkout (the default), | |
9 | # and deploying by export. | |
10 | class Remote < Base | |
11 | # Executes the SCM command for this strategy and writes the REVISION | |
12 | # mark file to each host. | |
13 | def deploy! | |
14 | scm_run "#{command} && #{mark}" | |
15 | end | |
16 | ||
17 | def check! | |
18 | super.check do |d| | |
19 | d.remote.command(source.command) | |
20 | end | |
21 | end | |
22 | ||
23 | protected | |
24 | ||
25 | # Runs the given command, filtering output back through the | |
26 | # #handle_data filter of the SCM implementation. | |
27 | def scm_run(command) | |
28 | run(command) do |ch,stream,text| | |
29 | ch[:state] ||= { :channel => ch } | |
30 | output = source.handle_data(ch[:state], stream, text) | |
31 | ch.send_data(output) if output | |
32 | end | |
33 | end | |
34 | ||
35 | # An abstract method which must be overridden in subclasses, to | |
36 | # return the actual SCM command(s) which must be executed on each | |
37 | # target host in order to perform the deployment. | |
38 | def command | |
39 | raise NotImplementedError, "`command' is not implemented by #{self.class.name}" | |
40 | end | |
41 | ||
42 | # Returns the command which will write the identifier of the | |
43 | # revision being deployed to the REVISION file on each host. | |
44 | def mark | |
45 | "(echo #{revision} > #{configuration[:release_path]}/REVISION)" | |
46 | end | |
47 | end | |
48 | ||
49 | end | |
50 | end | |
51 | end |
0 | require 'capistrano/recipes/deploy/strategy/remote' | |
1 | ||
2 | module Capistrano | |
3 | module Deploy | |
4 | module Strategy | |
5 | ||
6 | # Implements the deployment strategy that keeps a cached checkout of | |
7 | # the source code on each remote server. Each deploy simply updates the | |
8 | # cached checkout, and then does a copy from the cached copy to the | |
9 | # final deployment location. | |
10 | class RemoteCache < Remote | |
11 | # Executes the SCM command for this strategy and writes the REVISION | |
12 | # mark file to each host. | |
13 | def deploy! | |
14 | update_repository_cache | |
15 | copy_repository_cache | |
16 | end | |
17 | ||
18 | def check! | |
19 | super.check do |d| | |
20 | d.remote.command("rsync") unless copy_exclude.empty? | |
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 | require 'capistrano/recipes/deploy/strategy/remote_cache' | |
1 | ||
2 | module Capistrano | |
3 | module Deploy | |
4 | module Strategy | |
5 | class UnsharedRemoteCache < RemoteCache | |
6 | def check! | |
7 | super.check do |d| | |
8 | d.remote.writable(repository_cache) | |
9 | end | |
10 | end | |
11 | ||
12 | private | |
13 | ||
14 | def repository_cache | |
15 | configuration[:repository_cache] | |
16 | end | |
17 | end | |
18 | end | |
19 | end | |
20 | end |
0 | module Capistrano | |
1 | module Deploy | |
2 | module Strategy | |
3 | def self.new(strategy, config={}) | |
4 | strategy_file = "capistrano/recipes/deploy/strategy/#{strategy}" | |
5 | require(strategy_file) | |
6 | ||
7 | strategy_const = strategy.to_s.capitalize.gsub(/_(.)/) { $1.upcase } | |
8 | if const_defined?(strategy_const) | |
9 | const_get(strategy_const).new(config) | |
10 | else | |
11 | raise Capistrano::Error, "could not find `#{name}::#{strategy_const}' in `#{strategy_file}'" | |
12 | end | |
13 | rescue LoadError | |
14 | raise Capistrano::Error, "could not find any strategy named `#{strategy}'" | |
15 | end | |
16 | end | |
17 | end | |
18 | end⏎ |
0 | ||
1 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" | |
2 | "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | |
3 | ||
4 | <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> | |
5 | ||
6 | <head> | |
7 | <meta http-equiv="content-type" content="text/html;charset=UTF-8" /> | |
8 | <title>System down for maintenance</title> | |
9 | ||
10 | <style type="text/css"> | |
11 | div.outer { | |
12 | position: absolute; | |
13 | left: 50%; | |
14 | top: 50%; | |
15 | width: 500px; | |
16 | height: 300px; | |
17 | margin-left: -260px; | |
18 | margin-top: -150px; | |
19 | } | |
20 | ||
21 | .DialogBody { | |
22 | margin: 0; | |
23 | padding: 10px; | |
24 | text-align: left; | |
25 | border: 1px solid #ccc; | |
26 | border-right: 1px solid #999; | |
27 | border-bottom: 1px solid #999; | |
28 | background-color: #fff; | |
29 | } | |
30 | ||
31 | body { background-color: #fff; } | |
32 | </style> | |
33 | </head> | |
34 | ||
35 | <body> | |
36 | ||
37 | <div class="outer"> | |
38 | <div class="DialogBody" style="text-align: center;"> | |
39 | <div style="text-align: center; width: 200px; margin: 0 auto;"> | |
40 | <p style="color: red; font-size: 16px; line-height: 20px;"> | |
41 | The system is down for <%= reason ? reason : "maintenance" %> | |
42 | as of <%= Time.now.strftime("%H:%M %Z") %>. | |
43 | </p> | |
44 | <p style="color: #666;"> | |
45 | It'll be back <%= deadline ? deadline : "shortly" %>. | |
46 | </p> | |
47 | </div> | |
48 | </div> | |
49 | </div> | |
50 | ||
51 | </body> | |
52 | </html> |
0 | require 'benchmark' | |
1 | require 'yaml' | |
2 | require 'capistrano/recipes/deploy/scm' | |
3 | require 'capistrano/recipes/deploy/strategy' | |
4 | ||
5 | def _cset(name, *args, &block) | |
6 | unless exists?(name) | |
7 | set(name, *args, &block) | |
8 | end | |
9 | end | |
10 | ||
11 | # ========================================================================= | |
12 | # These variables MUST be set in the client capfiles. If they are not set, | |
13 | # the deploy will fail with an error. | |
14 | # ========================================================================= | |
15 | ||
16 | _cset(:application) { abort "Please specify the name of your application, set :application, 'foo'" } | |
17 | _cset(:repository) { abort "Please specify the repository that houses your application's code, set :repository, 'foo'" } | |
18 | ||
19 | # ========================================================================= | |
20 | # These variables may be set in the client capfile if their default values | |
21 | # are not sufficient. | |
22 | # ========================================================================= | |
23 | ||
24 | _cset :scm, :subversion | |
25 | _cset :deploy_via, :checkout | |
26 | ||
27 | _cset(:deploy_to) { "/u/apps/#{application}" } | |
28 | _cset(:revision) { source.head } | |
29 | ||
30 | _cset :rails_env, "production" | |
31 | _cset :rake, "rake" | |
32 | ||
33 | _cset :maintenance_basename, "maintenance" | |
34 | _cset(:maintenance_template_path) { File.join(File.dirname(__FILE__), "templates", "maintenance.rhtml") } | |
35 | ||
36 | # ========================================================================= | |
37 | # These variables should NOT be changed unless you are very confident in | |
38 | # what you are doing. Make sure you understand all the implications of your | |
39 | # changes if you do decide to muck with these! | |
40 | # ========================================================================= | |
41 | ||
42 | _cset(:source) { Capistrano::Deploy::SCM.new(scm, self) } | |
43 | _cset(:real_revision) { source.local.query_revision(revision) { |cmd| with_env("LC_ALL", "C") { run_locally(cmd) } } } | |
44 | ||
45 | _cset(:strategy) { Capistrano::Deploy::Strategy.new(deploy_via, self) } | |
46 | ||
47 | # If overriding release name, please also select an appropriate setting for :releases below. | |
48 | _cset(:release_name) { set :deploy_timestamped, true; Time.now.utc.strftime("%Y%m%d%H%M%S") } | |
49 | ||
50 | _cset :version_dir, "releases" | |
51 | _cset :shared_dir, "shared" | |
52 | _cset :shared_children, %w(public/system log tmp/pids) | |
53 | _cset :current_dir, "current" | |
54 | ||
55 | _cset(:releases_path) { File.join(deploy_to, version_dir) } | |
56 | _cset(:shared_path) { File.join(deploy_to, shared_dir) } | |
57 | _cset(:current_path) { File.join(deploy_to, current_dir) } | |
58 | _cset(:release_path) { File.join(releases_path, release_name) } | |
59 | ||
60 | _cset(:releases) { capture("ls -x #{releases_path}", :except => { :no_release => true }).split.sort } | |
61 | _cset(:current_release) { releases.length > 0 ? File.join(releases_path, releases.last) : nil } | |
62 | _cset(:previous_release) { releases.length > 1 ? File.join(releases_path, releases[-2]) : nil } | |
63 | ||
64 | _cset(:current_revision) { capture("cat #{current_path}/REVISION", :except => { :no_release => true }).chomp } | |
65 | _cset(:latest_revision) { capture("cat #{current_release}/REVISION", :except => { :no_release => true }).chomp } | |
66 | _cset(:previous_revision) { capture("cat #{previous_release}/REVISION", :except => { :no_release => true }).chomp if previous_release } | |
67 | ||
68 | _cset(:run_method) { fetch(:use_sudo, true) ? :sudo : :run } | |
69 | ||
70 | # some tasks, like symlink, need to always point at the latest release, but | |
71 | # they can also (occassionally) be called standalone. In the standalone case, | |
72 | # the timestamped release_path will be inaccurate, since the directory won't | |
73 | # actually exist. This variable lets tasks like symlink work either in the | |
74 | # standalone case, or during deployment. | |
75 | _cset(:latest_release) { exists?(:deploy_timestamped) ? release_path : current_release } | |
76 | ||
77 | # ========================================================================= | |
78 | # These are helper methods that will be available to your recipes. | |
79 | # ========================================================================= | |
80 | ||
81 | # Auxiliary helper method for the `deploy:check' task. Lets you set up your | |
82 | # own dependencies. | |
83 | def depend(location, type, *args) | |
84 | deps = fetch(:dependencies, {}) | |
85 | deps[location] ||= {} | |
86 | deps[location][type] ||= [] | |
87 | deps[location][type] << args | |
88 | set :dependencies, deps | |
89 | end | |
90 | ||
91 | # Temporarily sets an environment variable, yields to a block, and restores | |
92 | # the value when it is done. | |
93 | def with_env(name, value) | |
94 | saved, ENV[name] = ENV[name], value | |
95 | yield | |
96 | ensure | |
97 | ENV[name] = saved | |
98 | end | |
99 | ||
100 | # logs the command then executes it locally. | |
101 | # returns the command output as a string | |
102 | def run_locally(cmd) | |
103 | logger.trace "executing locally: #{cmd.inspect}" if logger | |
104 | output_on_stdout = nil | |
105 | elapsed = Benchmark.realtime do | |
106 | output_on_stdout = `#{cmd}` | |
107 | end | |
108 | if $?.to_i > 0 # $? is command exit code (posix style) | |
109 | raise Capistrano::LocalArgumentError, "Command #{cmd} returned status code #{$?}" | |
110 | end | |
111 | logger.trace "command finished in #{(elapsed * 1000).round}ms" if logger | |
112 | output_on_stdout | |
113 | end | |
114 | ||
115 | ||
116 | # If a command is given, this will try to execute the given command, as | |
117 | # described below. Otherwise, it will return a string for use in embedding in | |
118 | # another command, for executing that command as described below. | |
119 | # | |
120 | # If :run_method is :sudo (or :use_sudo is true), this executes the given command | |
121 | # via +sudo+. Otherwise is uses +run+. If :as is given as a key, it will be | |
122 | # passed as the user to sudo as, if using sudo. If the :as key is not given, | |
123 | # it will default to whatever the value of the :admin_runner variable is, | |
124 | # which (by default) is unset. | |
125 | # | |
126 | # THUS, if you want to try to run something via sudo, and what to use the | |
127 | # root user, you'd just to try_sudo('something'). If you wanted to try_sudo as | |
128 | # someone else, you'd just do try_sudo('something', :as => "bob"). If you | |
129 | # always wanted sudo to run as a particular user, you could do | |
130 | # set(:admin_runner, "bob"). | |
131 | def try_sudo(*args) | |
132 | options = args.last.is_a?(Hash) ? args.pop : {} | |
133 | command = args.shift | |
134 | raise ArgumentError, "too many arguments" if args.any? | |
135 | ||
136 | as = options.fetch(:as, fetch(:admin_runner, nil)) | |
137 | via = fetch(:run_method, :sudo) | |
138 | if command | |
139 | invoke_command(command, :via => via, :as => as) | |
140 | elsif via == :sudo | |
141 | sudo(:as => as) | |
142 | else | |
143 | "" | |
144 | end | |
145 | end | |
146 | ||
147 | # Same as sudo, but tries sudo with :as set to the value of the :runner | |
148 | # variable (which defaults to "app"). | |
149 | def try_runner(*args) | |
150 | options = args.last.is_a?(Hash) ? args.pop : {} | |
151 | args << options.merge(:as => fetch(:runner, "app")) | |
152 | try_sudo(*args) | |
153 | end | |
154 | ||
155 | # ========================================================================= | |
156 | # These are the tasks that are available to help with deploying web apps, | |
157 | # and specifically, Rails applications. You can have cap give you a summary | |
158 | # of them with `cap -T'. | |
159 | # ========================================================================= | |
160 | ||
161 | namespace :deploy do | |
162 | desc <<-DESC | |
163 | Deploys your project. This calls both `update' and `restart'. Note that \ | |
164 | this will generally only work for applications that have already been deployed \ | |
165 | once. For a "cold" deploy, you'll want to take a look at the `deploy:cold' \ | |
166 | task, which handles the cold start specifically. | |
167 | DESC | |
168 | task :default do | |
169 | update | |
170 | restart | |
171 | end | |
172 | ||
173 | desc <<-DESC | |
174 | Prepares one or more servers for deployment. Before you can use any \ | |
175 | of the Capistrano deployment tasks with your project, you will need to \ | |
176 | make sure all of your servers have been prepared with `cap deploy:setup'. When \ | |
177 | you add a new server to your cluster, you can easily run the setup task \ | |
178 | on just that server by specifying the HOSTS environment variable: | |
179 | ||
180 | $ cap HOSTS=new.server.com deploy:setup | |
181 | ||
182 | It is safe to run this task on servers that have already been set up; it \ | |
183 | will not destroy any deployed revisions or data. | |
184 | DESC | |
185 | task :setup, :except => { :no_release => true } do | |
186 | dirs = [deploy_to, releases_path, shared_path] | |
187 | dirs += shared_children.map { |d| File.join(shared_path, d.split('/').last) } | |
188 | run "#{try_sudo} mkdir -p #{dirs.join(' ')}" | |
189 | run "#{try_sudo} chmod g+w #{dirs.join(' ')}" if fetch(:group_writable, true) | |
190 | end | |
191 | ||
192 | desc <<-DESC | |
193 | Copies your project and updates the symlink. It does this in a \ | |
194 | transaction, so that if either `update_code' or `symlink' fail, all \ | |
195 | changes made to the remote servers will be rolled back, leaving your \ | |
196 | system in the same state it was in before `update' was invoked. Usually, \ | |
197 | you will want to call `deploy' instead of `update', but `update' can be \ | |
198 | handy if you want to deploy, but not immediately restart your application. | |
199 | DESC | |
200 | task :update do | |
201 | transaction do | |
202 | update_code | |
203 | create_symlink | |
204 | end | |
205 | end | |
206 | ||
207 | desc <<-DESC | |
208 | Copies your project to the remote servers. This is the first stage \ | |
209 | of any deployment; moving your updated code and assets to the deployment \ | |
210 | servers. You will rarely call this task directly, however; instead, you \ | |
211 | should call the `deploy' task (to do a complete deploy) or the `update' \ | |
212 | task (if you want to perform the `restart' task separately). | |
213 | ||
214 | You will need to make sure you set the :scm variable to the source \ | |
215 | control software you are using (it defaults to :subversion), and the \ | |
216 | :deploy_via variable to the strategy you want to use to deploy (it \ | |
217 | defaults to :checkout). | |
218 | DESC | |
219 | task :update_code, :except => { :no_release => true } do | |
220 | on_rollback { run "rm -rf #{release_path}; true" } | |
221 | strategy.deploy! | |
222 | finalize_update | |
223 | end | |
224 | ||
225 | desc <<-DESC | |
226 | [internal] Touches up the released code. This is called by update_code \ | |
227 | after the basic deploy finishes. It assumes a Rails project was deployed, \ | |
228 | so if you are deploying something else, you may want to override this \ | |
229 | task with your own environment's requirements. | |
230 | ||
231 | This task will make the release group-writable (if the :group_writable \ | |
232 | variable is set to true, which is the default). It will then set up \ | |
233 | symlinks to the shared directory for the log, system, and tmp/pids \ | |
234 | directories, and will lastly touch all assets in public/images, \ | |
235 | public/stylesheets, and public/javascripts so that the times are \ | |
236 | consistent (so that asset timestamping works). This touch process \ | |
237 | is only carried out if the :normalize_asset_timestamps variable is \ | |
238 | set to true, which is the default The asset directories can be overridden \ | |
239 | using the :public_children variable. | |
240 | DESC | |
241 | task :finalize_update, :except => { :no_release => true } do | |
242 | run "chmod -R g+w #{latest_release}" if fetch(:group_writable, true) | |
243 | ||
244 | # mkdir -p is making sure that the directories are there for some SCM's that don't | |
245 | # save empty folders | |
246 | run <<-CMD | |
247 | rm -rf #{latest_release}/log #{latest_release}/public/system #{latest_release}/tmp/pids && | |
248 | mkdir -p #{latest_release}/public && | |
249 | mkdir -p #{latest_release}/tmp | |
250 | CMD | |
251 | shared_children.map do |d| | |
252 | run "ln -s #{shared_path}/#{d.split('/').last} #{latest_release}/#{d}" | |
253 | end | |
254 | ||
255 | if fetch(:normalize_asset_timestamps, true) | |
256 | stamp = Time.now.utc.strftime("%Y%m%d%H%M.%S") | |
257 | asset_paths = fetch(:public_children, %w(images stylesheets javascripts)).map { |p| "#{latest_release}/public/#{p}" }.join(" ") | |
258 | run "find #{asset_paths} -exec touch -t #{stamp} {} ';'; true", :env => { "TZ" => "UTC" } | |
259 | end | |
260 | end | |
261 | ||
262 | desc <<-DESC | |
263 | Deprecated API. This has become deploy:create_symlink, please update your recipes | |
264 | DESC | |
265 | task :symlink, :except => { :no_release => true } do | |
266 | Kernel.warn "[Deprecation Warning] This API has changed, please hook `deploy:create_symlink` instead of `deploy:symlink`." | |
267 | create_symlink | |
268 | end | |
269 | ||
270 | desc <<-DESC | |
271 | Updates the symlink to the most recently deployed version. Capistrano works \ | |
272 | by putting each new release of your application in its own directory. When \ | |
273 | you deploy a new version, this task's job is to update the `current' symlink \ | |
274 | to point at the new version. You will rarely need to call this task \ | |
275 | directly; instead, use the `deploy' task (which performs a complete \ | |
276 | deploy, including `restart') or the 'update' task (which does everything \ | |
277 | except `restart'). | |
278 | DESC | |
279 | task :create_symlink, :except => { :no_release => true } do | |
280 | on_rollback do | |
281 | if previous_release | |
282 | run "rm -f #{current_path}; ln -s #{previous_release} #{current_path}; true" | |
283 | else | |
284 | logger.important "no previous release to rollback to, rollback of symlink skipped" | |
285 | end | |
286 | end | |
287 | ||
288 | run "rm -f #{current_path} && ln -s #{latest_release} #{current_path}" | |
289 | end | |
290 | ||
291 | desc <<-DESC | |
292 | Copy files to the currently deployed version. This is useful for updating \ | |
293 | files piecemeal, such as when you need to quickly deploy only a single \ | |
294 | file. Some files, such as updated templates, images, or stylesheets, \ | |
295 | might not require a full deploy, and especially in emergency situations \ | |
296 | it can be handy to just push the updates to production, quickly. | |
297 | ||
298 | To use this task, specify the files and directories you want to copy as a \ | |
299 | comma-delimited list in the FILES environment variable. All directories \ | |
300 | will be processed recursively, with all files being pushed to the \ | |
301 | deployment servers. | |
302 | ||
303 | $ cap deploy:upload FILES=templates,controller.rb | |
304 | ||
305 | Dir globs are also supported: | |
306 | ||
307 | $ cap deploy:upload FILES='config/apache/*.conf' | |
308 | DESC | |
309 | task :upload, :except => { :no_release => true } do | |
310 | files = (ENV["FILES"] || "").split(",").map { |f| Dir[f.strip] }.flatten | |
311 | abort "Please specify at least one file or directory to update (via the FILES environment variable)" if files.empty? | |
312 | ||
313 | files.each { |file| top.upload(file, File.join(current_path, file)) } | |
314 | end | |
315 | ||
316 | desc <<-DESC | |
317 | Blank task exists as a hook into which to install your own environment \ | |
318 | specific behaviour. | |
319 | DESC | |
320 | task :restart, :roles => :app, :except => { :no_release => true } do | |
321 | # Empty Task to overload with your platform specifics | |
322 | end | |
323 | ||
324 | namespace :rollback do | |
325 | desc <<-DESC | |
326 | [internal] Points the current symlink at the previous revision. | |
327 | This is called by the rollback sequence, and should rarely (if | |
328 | ever) need to be called directly. | |
329 | DESC | |
330 | task :revision, :except => { :no_release => true } do | |
331 | if previous_release | |
332 | run "rm #{current_path}; ln -s #{previous_release} #{current_path}" | |
333 | else | |
334 | abort "could not rollback the code because there is no prior release" | |
335 | end | |
336 | end | |
337 | ||
338 | desc <<-DESC | |
339 | [internal] Removes the most recently deployed release. | |
340 | This is called by the rollback sequence, and should rarely | |
341 | (if ever) need to be called directly. | |
342 | DESC | |
343 | task :cleanup, :except => { :no_release => true } do | |
344 | run "if [ `readlink #{current_path}` != #{current_release} ]; then rm -rf #{current_release}; fi" | |
345 | end | |
346 | ||
347 | desc <<-DESC | |
348 | Rolls back to the previously deployed version. The `current' symlink will \ | |
349 | be updated to point at the previously deployed version, and then the \ | |
350 | current release will be removed from the servers. You'll generally want \ | |
351 | to call `rollback' instead, as it performs a `restart' as well. | |
352 | DESC | |
353 | task :code, :except => { :no_release => true } do | |
354 | revision | |
355 | cleanup | |
356 | end | |
357 | ||
358 | desc <<-DESC | |
359 | Rolls back to a previous version and restarts. This is handy if you ever \ | |
360 | discover that you've deployed a lemon; `cap rollback' and you're right \ | |
361 | back where you were, on the previously deployed version. | |
362 | DESC | |
363 | task :default do | |
364 | revision | |
365 | restart | |
366 | cleanup | |
367 | end | |
368 | end | |
369 | ||
370 | desc <<-DESC | |
371 | Run the migrate rake task. By default, it runs this in most recently \ | |
372 | deployed version of the app. However, you can specify a different release \ | |
373 | via the migrate_target variable, which must be one of :latest (for the \ | |
374 | default behavior), or :current (for the release indicated by the \ | |
375 | `current' symlink). Strings will work for those values instead of symbols, \ | |
376 | too. You can also specify additional environment variables to pass to rake \ | |
377 | via the migrate_env variable. Finally, you can specify the full path to the \ | |
378 | rake executable by setting the rake variable. The defaults are: | |
379 | ||
380 | set :rake, "rake" | |
381 | set :rails_env, "production" | |
382 | set :migrate_env, "" | |
383 | set :migrate_target, :latest | |
384 | DESC | |
385 | task :migrate, :roles => :db, :only => { :primary => true } do | |
386 | rake = fetch(:rake, "rake") | |
387 | rails_env = fetch(:rails_env, "production") | |
388 | migrate_env = fetch(:migrate_env, "") | |
389 | migrate_target = fetch(:migrate_target, :latest) | |
390 | ||
391 | directory = case migrate_target.to_sym | |
392 | when :current then current_path | |
393 | when :latest then latest_release | |
394 | else raise ArgumentError, "unknown migration target #{migrate_target.inspect}" | |
395 | end | |
396 | ||
397 | run "cd #{directory} && #{rake} RAILS_ENV=#{rails_env} #{migrate_env} db:migrate" | |
398 | end | |
399 | ||
400 | desc <<-DESC | |
401 | Deploy and run pending migrations. This will work similarly to the \ | |
402 | `deploy' task, but will also run any pending migrations (via the \ | |
403 | `deploy:migrate' task) prior to updating the symlink. Note that the \ | |
404 | update in this case it is not atomic, and transactions are not used, \ | |
405 | because migrations are not guaranteed to be reversible. | |
406 | DESC | |
407 | task :migrations do | |
408 | set :migrate_target, :latest | |
409 | update_code | |
410 | migrate | |
411 | create_symlink | |
412 | restart | |
413 | end | |
414 | ||
415 | desc <<-DESC | |
416 | Clean up old releases. By default, the last 5 releases are kept on each \ | |
417 | server (though you can change this with the keep_releases variable). All \ | |
418 | other deployed revisions are removed from the servers. By default, this \ | |
419 | will use sudo to clean up the old releases, but if sudo is not available \ | |
420 | for your environment, set the :use_sudo variable to false instead. | |
421 | DESC | |
422 | task :cleanup, :except => { :no_release => true } do | |
423 | count = fetch(:keep_releases, 5).to_i | |
424 | local_releases = capture("ls -xt #{releases_path}").split.reverse | |
425 | if count >= local_releases.length | |
426 | logger.important "no old releases to clean up" | |
427 | else | |
428 | logger.info "keeping #{count} of #{local_releases.length} deployed releases" | |
429 | directories = (local_releases - local_releases.last(count)).map { |release| | |
430 | File.join(releases_path, release) }.join(" ") | |
431 | ||
432 | try_sudo "rm -rf #{directories}" | |
433 | end | |
434 | end | |
435 | ||
436 | desc <<-DESC | |
437 | Test deployment dependencies. Checks things like directory permissions, \ | |
438 | necessary utilities, and so forth, reporting on the things that appear to \ | |
439 | be incorrect or missing. This is good for making sure a deploy has a \ | |
440 | chance of working before you actually run `cap deploy'. | |
441 | ||
442 | You can define your own dependencies, as well, using the `depend' method: | |
443 | ||
444 | depend :remote, :gem, "tzinfo", ">=0.3.3" | |
445 | depend :local, :command, "svn" | |
446 | depend :remote, :directory, "/u/depot/files" | |
447 | DESC | |
448 | task :check, :except => { :no_release => true } do | |
449 | dependencies = strategy.check! | |
450 | ||
451 | other = fetch(:dependencies, {}) | |
452 | other.each do |location, types| | |
453 | types.each do |type, calls| | |
454 | if type == :gem | |
455 | dependencies.send(location).command(fetch(:gem_command, "gem")).or("`gem' command could not be found. Try setting :gem_command") | |
456 | end | |
457 | ||
458 | calls.each do |args| | |
459 | dependencies.send(location).send(type, *args) | |
460 | end | |
461 | end | |
462 | end | |
463 | ||
464 | if dependencies.pass? | |
465 | puts "You appear to have all necessary dependencies installed" | |
466 | else | |
467 | puts "The following dependencies failed. Please check them and try again:" | |
468 | dependencies.reject { |d| d.pass? }.each do |d| | |
469 | puts "--> #{d.message}" | |
470 | end | |
471 | abort | |
472 | end | |
473 | end | |
474 | ||
475 | desc <<-DESC | |
476 | Deploys and starts a `cold' application. This is useful if you have not \ | |
477 | deployed your application before, or if your application is (for some \ | |
478 | other reason) not currently running. It will deploy the code, run any \ | |
479 | pending migrations, and then instead of invoking `deploy:restart', it will \ | |
480 | invoke `deploy:start' to fire up the application servers. | |
481 | DESC | |
482 | task :cold do | |
483 | update | |
484 | migrate | |
485 | start | |
486 | end | |
487 | ||
488 | desc <<-DESC | |
489 | Blank task exists as a hook into which to install your own environment \ | |
490 | specific behaviour. | |
491 | DESC | |
492 | task :start, :roles => :app do | |
493 | # Empty Task to overload with your platform specifics | |
494 | end | |
495 | ||
496 | desc <<-DESC | |
497 | Blank task exists as a hook into which to install your own environment \ | |
498 | specific behaviour. | |
499 | DESC | |
500 | task :stop, :roles => :app do | |
501 | # Empty Task to overload with your platform specifics | |
502 | end | |
503 | ||
504 | namespace :pending do | |
505 | desc <<-DESC | |
506 | Displays the `diff' since your last deploy. This is useful if you want \ | |
507 | to examine what changes are about to be deployed. Note that this might \ | |
508 | not be supported on all SCM's. | |
509 | DESC | |
510 | task :diff, :except => { :no_release => true } do | |
511 | system(source.local.diff(current_revision)) | |
512 | end | |
513 | ||
514 | desc <<-DESC | |
515 | Displays the commits since your last deploy. This is good for a summary \ | |
516 | of the changes that have occurred since the last deploy. Note that this \ | |
517 | might not be supported on all SCM's. | |
518 | DESC | |
519 | task :default, :except => { :no_release => true } do | |
520 | from = source.next_revision(current_revision) | |
521 | system(source.local.log(from)) | |
522 | end | |
523 | end | |
524 | ||
525 | namespace :web do | |
526 | desc <<-DESC | |
527 | Present a maintenance page to visitors. Disables your application's web \ | |
528 | interface by writing a "#{maintenance_basename}.html" file to each web server. The \ | |
529 | servers must be configured to detect the presence of this file, and if \ | |
530 | it is present, always display it instead of performing the request. | |
531 | ||
532 | By default, the maintenance page will just say the site is down for \ | |
533 | "maintenance", and will be back "shortly", but you can customize the \ | |
534 | page by specifying the REASON and UNTIL environment variables: | |
535 | ||
536 | $ cap deploy:web:disable \\ | |
537 | REASON="hardware upgrade" \\ | |
538 | UNTIL="12pm Central Time" | |
539 | ||
540 | You can use a different template for the maintenance page by setting the \ | |
541 | :maintenance_template_path variable in your deploy.rb file. The template file \ | |
542 | should either be a plaintext or an erb file. | |
543 | ||
544 | Further customization will require that you write your own task. | |
545 | DESC | |
546 | task :disable, :roles => :web, :except => { :no_release => true } do | |
547 | require 'erb' | |
548 | on_rollback { run "rm -f #{shared_path}/system/#{maintenance_basename}.html" } | |
549 | ||
550 | warn <<-EOHTACCESS | |
551 | ||
552 | # Please add something like this to your site's Apache htaccess to redirect users to the maintenance page. | |
553 | # More Info: http://www.shiftcommathree.com/articles/make-your-rails-maintenance-page-respond-with-a-503 | |
554 | ||
555 | ErrorDocument 503 /system/#{maintenance_basename}.html | |
556 | RewriteEngine On | |
557 | RewriteCond %{REQUEST_URI} !\.(css|gif|jpg|png)$ | |
558 | RewriteCond %{DOCUMENT_ROOT}/system/#{maintenance_basename}.html -f | |
559 | RewriteCond %{SCRIPT_FILENAME} !#{maintenance_basename}.html | |
560 | RewriteRule ^.*$ - [redirect=503,last] | |
561 | ||
562 | # Or if you are using Nginx add this to your server config: | |
563 | ||
564 | if (-f $document_root/system/maintenance.html) { | |
565 | return 503; | |
566 | } | |
567 | error_page 503 @maintenance; | |
568 | location @maintenance { | |
569 | rewrite ^(.*)$ /system/maintenance.html last; | |
570 | break; | |
571 | } | |
572 | EOHTACCESS | |
573 | ||
574 | reason = ENV['REASON'] | |
575 | deadline = ENV['UNTIL'] | |
576 | ||
577 | template = File.read(maintenance_template_path) | |
578 | result = ERB.new(template).result(binding) | |
579 | ||
580 | put result, "#{shared_path}/system/#{maintenance_basename}.html", :mode => 0644 | |
581 | end | |
582 | ||
583 | desc <<-DESC | |
584 | Makes the application web-accessible again. Removes the \ | |
585 | "#{maintenance_basename}.html" page generated by deploy:web:disable, which (if your \ | |
586 | web servers are configured correctly) will make your application \ | |
587 | web-accessible again. | |
588 | DESC | |
589 | task :enable, :roles => :web, :except => { :no_release => true } do | |
590 | run "rm -f #{shared_path}/system/#{maintenance_basename}.html" | |
591 | end | |
592 | end | |
593 | end |
0 | desc <<-DESC | |
1 | Invoke a single command on the remote servers. This is useful for performing \ | |
2 | one-off commands that may not require a full task to be written for them. \ | |
3 | Simply specify the command to execute via the COMMAND environment variable. \ | |
4 | To execute the command only on certain roles, specify the ROLES environment \ | |
5 | variable as a comma-delimited list of role names. Alternatively, you can \ | |
6 | specify the HOSTS environment variable as a comma-delimited list of hostnames \ | |
7 | to execute the task on those hosts, explicitly. Lastly, if you want to \ | |
8 | execute the command via sudo, specify a non-empty value for the SUDO \ | |
9 | environment variable. | |
10 | ||
11 | Sample usage: | |
12 | ||
13 | $ cap COMMAND=uptime HOSTS=foo.capistano.test invoke | |
14 | $ cap ROLES=app,web SUDO=1 COMMAND="tail -f /var/log/messages" invoke | |
15 | DESC | |
16 | task :invoke do | |
17 | command = ENV["COMMAND"] || "" | |
18 | abort "Please specify a command to execute on the remote servers (via the COMMAND environment variable)" if command.empty? | |
19 | method = ENV["SUDO"] ? :sudo : :run | |
20 | invoke_command(command, :via => method) | |
21 | end | |
22 | ||
23 | desc <<-DESC | |
24 | Begin an interactive Capistrano session. This gives you an interactive \ | |
25 | terminal from which to execute tasks and commands on all of your servers. \ | |
26 | (This is still an experimental feature, and is subject to change without \ | |
27 | notice!) | |
28 | ||
29 | Sample usage: | |
30 | ||
31 | $ cap shell | |
32 | DESC | |
33 | task :shell do | |
34 | require 'capistrano/shell' | |
35 | Capistrano::Shell.run(self) | |
36 | end⏎ |
0 | ||
1 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" | |
2 | "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> | |
3 | ||
4 | <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> | |
5 | ||
6 | <head> | |
7 | <meta http-equiv="content-type" content="text/html;charset=UTF-8" /> | |
8 | <title>System down for maintenance</title> | |
9 | ||
10 | <style type="text/css"> | |
11 | div.outer { | |
12 | position: absolute; | |
13 | left: 50%; | |
14 | top: 50%; | |
15 | width: 500px; | |
16 | height: 300px; | |
17 | margin-left: -260px; | |
18 | margin-top: -150px; | |
19 | } | |
20 | ||
21 | .DialogBody { | |
22 | margin: 0; | |
23 | padding: 10px; | |
24 | text-align: left; | |
25 | border: 1px solid #ccc; | |
26 | border-right: 1px solid #999; | |
27 | border-bottom: 1px solid #999; | |
28 | background-color: #fff; | |
29 | } | |
30 | ||
31 | body { background-color: #fff; } | |
32 | </style> | |
33 | </head> | |
34 | ||
35 | <body> | |
36 | ||
37 | <div class="outer"> | |
38 | <div class="DialogBody" style="text-align: center;"> | |
39 | <div style="text-align: center; width: 200px; margin: 0 auto;"> | |
40 | <p style="color: red; font-size: 16px; line-height: 20px;"> | |
41 | The system is down for <%= reason ? reason : "maintenance" %> | |
42 | as of <%= Time.now.strftime("%H:%M %Z") %>. | |
43 | </p> | |
44 | <p style="color: #666;"> | |
45 | It'll be back <%= deadline ? deadline : "shortly" %>. | |
46 | </p> | |
47 | </div> | |
48 | </div> | |
49 | </div> | |
50 | ||
51 | </body> | |
52 | </html> |
0 | module Capistrano | |
1 | class Role | |
2 | include Enumerable | |
3 | ||
4 | def initialize(*list) | |
5 | @static_servers = [] | |
6 | @dynamic_servers = [] | |
7 | push(*list) | |
8 | end | |
9 | ||
10 | def each(&block) | |
11 | servers.each &block | |
12 | end | |
13 | ||
14 | def push(*list) | |
15 | options = list.last.is_a?(Hash) ? list.pop : {} | |
16 | list.each do |item| | |
17 | if item.respond_to?(:call) | |
18 | @dynamic_servers << DynamicServerList.new(item, options) | |
19 | else | |
20 | @static_servers << self.class.wrap_server(item, options) | |
21 | end | |
22 | end | |
23 | end | |
24 | alias_method :<<, :push | |
25 | ||
26 | def servers | |
27 | @static_servers + dynamic_servers | |
28 | end | |
29 | alias_method :to_ary, :servers | |
30 | ||
31 | def empty? | |
32 | servers.empty? | |
33 | end | |
34 | ||
35 | def clear | |
36 | @dynamic_servers.clear | |
37 | @static_servers.clear | |
38 | end | |
39 | ||
40 | def include?(server) | |
41 | servers.include?(server) | |
42 | end | |
43 | ||
44 | protected | |
45 | ||
46 | # This is the combination of a block, a hash of options, and a cached value. | |
47 | class DynamicServerList | |
48 | def initialize (block, options) | |
49 | @block = block | |
50 | @options = options | |
51 | @cached = [] | |
52 | @is_cached = false | |
53 | end | |
54 | ||
55 | # Convert to a list of ServerDefinitions | |
56 | def to_ary | |
57 | unless @is_cached | |
58 | @cached = Role::wrap_list(@block.call(@options), @options) | |
59 | @is_cached = true | |
60 | end | |
61 | @cached | |
62 | end | |
63 | ||
64 | # Clear the cached value | |
65 | def reset! | |
66 | @cached.clear | |
67 | @is_cached = false | |
68 | end | |
69 | end | |
70 | ||
71 | # Attribute reader for the cached results of executing the blocks in turn | |
72 | def dynamic_servers | |
73 | @dynamic_servers.inject([]) { |list, item| list.concat item } | |
74 | end | |
75 | ||
76 | # Wraps a string in a ServerDefinition, if it isn't already. | |
77 | # This and wrap_list should probably go in ServerDefinition in some form. | |
78 | def self.wrap_server (item, options) | |
79 | item.is_a?(ServerDefinition) ? item : ServerDefinition.new(item, options) | |
80 | end | |
81 | ||
82 | # Turns a list, or something resembling a list, into a properly-formatted | |
83 | # ServerDefinition list. Keep an eye on this one -- it's entirely too | |
84 | # magical for its own good. In particular, if ServerDefinition ever inherits | |
85 | # from Array, this will break. | |
86 | def self.wrap_list (*list) | |
87 | options = list.last.is_a?(Hash) ? list.pop : {} | |
88 | if list.length == 1 | |
89 | if list.first.nil? | |
90 | return [] | |
91 | elsif list.first.is_a?(Array) | |
92 | list = list.first | |
93 | end | |
94 | end | |
95 | options.merge! list.pop if list.last.is_a?(Hash) | |
96 | list.map do |item| | |
97 | self.wrap_server item, options | |
98 | end | |
99 | end | |
100 | end | |
101 | end |
0 | module Capistrano | |
1 | ||
2 | # Base class for SCM strategy providers. | |
3 | # | |
4 | # @abstract | |
5 | # | |
6 | # @attr_reader [Rake] context | |
7 | # | |
8 | # @author Hartog de Mik | |
9 | # | |
10 | class SCM | |
11 | attr_reader :context | |
12 | ||
13 | # Provide a wrapper for the SCM that loads a strategy for the user. | |
14 | # | |
15 | # @param [Rake] context The context in which the strategy should run | |
16 | # @param [Module] strategy A module to include into the SCM instance. The | |
17 | # module should provide the abstract methods of Capistrano::SCM | |
18 | # | |
19 | def initialize(context, strategy) | |
20 | @context = context | |
21 | singleton = class << self; self; end | |
22 | singleton.send(:include, strategy) | |
23 | end | |
24 | ||
25 | # Call test in context | |
26 | def test!(*args) | |
27 | context.test *args | |
28 | end | |
29 | ||
30 | # The repository URL according to the context | |
31 | def repo_url | |
32 | context.repo_url | |
33 | end | |
34 | ||
35 | # The repository path according to the context | |
36 | def repo_path | |
37 | context.repo_path | |
38 | end | |
39 | ||
40 | # The release path according to the context | |
41 | def release_path | |
42 | context.release_path | |
43 | end | |
44 | ||
45 | # Fetch a var from the context | |
46 | # @param [Symbol] variable The variable to fetch | |
47 | # @param [Object] default The default value if not found | |
48 | # | |
49 | def fetch(*args) | |
50 | context.fetch(*args) | |
51 | end | |
52 | ||
53 | # @abstract | |
54 | # | |
55 | # Your implementation should check the existence of a cache repository on | |
56 | # the deployment target | |
57 | # | |
58 | # @return [Boolean] | |
59 | # | |
60 | def test | |
61 | raise NotImplementedError.new( | |
62 | "Your SCM strategy module should provide a #test method" | |
63 | ) | |
64 | end | |
65 | ||
66 | # @abstract | |
67 | # | |
68 | # Your implementation should check if the specified remote-repository is | |
69 | # available. | |
70 | # | |
71 | # @return [Boolean] | |
72 | # | |
73 | def check | |
74 | raise NotImplementedError.new( | |
75 | "Your SCM strategy module should provide a #check method" | |
76 | ) | |
77 | end | |
78 | ||
79 | # @abstract | |
80 | # | |
81 | # Create a (new) clone of the remote-repository on the deployment target | |
82 | # | |
83 | # @return void | |
84 | # | |
85 | def clone | |
86 | raise NotImplementedError.new( | |
87 | "Your SCM strategy module should provide a #clone method" | |
88 | ) | |
89 | end | |
90 | ||
91 | # @abstract | |
92 | # | |
93 | # Update the clone on the deployment target | |
94 | # | |
95 | # @return void | |
96 | # | |
97 | def update | |
98 | raise NotImplementedError.new( | |
99 | "Your SCM strategy module should provide a #update method" | |
100 | ) | |
101 | end | |
102 | ||
103 | # @abstract | |
104 | # | |
105 | # Copy the contents of the cache-repository onto the release path | |
106 | # | |
107 | # @return void | |
108 | # | |
109 | def release | |
110 | raise NotImplementedError.new( | |
111 | "Your SCM strategy module should provide a #release method" | |
112 | ) | |
113 | end | |
114 | ||
115 | # @abstract | |
116 | # | |
117 | # Identify the SHA of the commit that will be deployed. This will most likely involve SshKit's capture method. | |
118 | # | |
119 | # @return void | |
120 | # | |
121 | def fetch_revision | |
122 | raise NotImplementedError.new( | |
123 | "Your SCM strategy module should provide a #fetch_revision method" | |
124 | ) | |
125 | end | |
126 | end | |
127 | end |
0 | module Capistrano | |
1 | class ServerDefinition | |
2 | include Comparable | |
3 | ||
4 | attr_reader :host | |
5 | attr_reader :user | |
6 | attr_reader :port | |
7 | attr_reader :options | |
8 | ||
9 | # The default user name to use when a user name is not explicitly provided | |
10 | def self.default_user | |
11 | ENV['USER'] || ENV['USERNAME'] || "not-specified" | |
12 | end | |
13 | ||
14 | def initialize(string, options={}) | |
15 | @user, @host, @port = string.match(/^(?:([^;,:=]+)@|)(.*?)(?::(\d+)|)$/)[1,3] | |
16 | ||
17 | @options = options.dup | |
18 | user_opt, port_opt = @options.delete(:user), @options.delete(:port) | |
19 | ||
20 | @user ||= user_opt | |
21 | @port ||= port_opt | |
22 | ||
23 | @port = @port.to_i if @port | |
24 | end | |
25 | ||
26 | def <=>(server) | |
27 | [host, port, user] <=> [server.host, server.port, server.user] | |
28 | end | |
29 | ||
30 | # Redefined, so that Array#uniq will work to remove duplicate server | |
31 | # definitions, based solely on their host names. | |
32 | def eql?(server) | |
33 | host == server.host && | |
34 | user == server.user && | |
35 | port == server.port | |
36 | end | |
37 | ||
38 | alias :== :eql? | |
39 | ||
40 | # Redefined, so that Array#uniq will work to remove duplicate server | |
41 | # definitions, based on their connection information. | |
42 | def hash | |
43 | @hash ||= [host, user, port].hash | |
44 | end | |
45 | ||
46 | def to_s | |
47 | @to_s ||= begin | |
48 | s = host | |
49 | s = "#{user}@#{s}" if user | |
50 | s = "#{s}:#{port}" if port && port != 22 | |
51 | s | |
52 | end | |
53 | end | |
54 | end | |
55 | end⏎ |
0 | include Capistrano::DSL | |
1 | ||
2 | namespace :load do | |
3 | task :defaults do | |
4 | load 'capistrano/defaults.rb' | |
5 | end | |
6 | end | |
7 | ||
8 | stages.each do |stage| | |
9 | Rake::Task.define_task(stage) do | |
10 | set(:stage, stage.to_sym) | |
11 | ||
12 | invoke 'load:defaults' | |
13 | load deploy_config_path | |
14 | load stage_config_path.join("#{stage}.rb") | |
15 | load "capistrano/#{fetch(:scm)}.rb" | |
16 | I18n.locale = fetch(:locale, :en) | |
17 | configure_backend | |
18 | end | |
19 | end | |
20 | ||
21 | require 'capistrano/dotfile' |
0 | require 'thread' | |
1 | require 'capistrano/processable' | |
2 | ||
3 | module Capistrano | |
4 | # The Capistrano::Shell class is the guts of the "shell" task. It implements | |
5 | # an interactive REPL interface that users can employ to execute tasks and | |
6 | # commands. It makes for a GREAT way to monitor systems, and perform quick | |
7 | # maintenance on one or more machines. | |
8 | class Shell | |
9 | include Processable | |
10 | ||
11 | # A Readline replacement for platforms where readline is either | |
12 | # unavailable, or has not been installed. | |
13 | class ReadlineFallback #:nodoc: | |
14 | HISTORY = [] | |
15 | ||
16 | def self.readline(prompt) | |
17 | STDOUT.print(prompt) | |
18 | STDOUT.flush | |
19 | STDIN.gets | |
20 | end | |
21 | end | |
22 | ||
23 | # The configuration instance employed by this shell | |
24 | attr_reader :configuration | |
25 | ||
26 | # Instantiate a new shell and begin executing it immediately. | |
27 | def self.run(config) | |
28 | new(config).run! | |
29 | end | |
30 | ||
31 | # Instantiate a new shell | |
32 | def initialize(config) | |
33 | @configuration = config | |
34 | end | |
35 | ||
36 | # Start the shell running. This method will block until the shell | |
37 | # terminates. | |
38 | def run! | |
39 | setup | |
40 | ||
41 | puts <<-INTRO | |
42 | ==================================================================== | |
43 | Welcome to the interactive Capistrano shell! This is an experimental | |
44 | feature, and is liable to change in future releases. Type 'help' for | |
45 | a summary of how to use the shell. | |
46 | -------------------------------------------------------------------- | |
47 | INTRO | |
48 | ||
49 | loop do | |
50 | break if !read_and_execute | |
51 | end | |
52 | ||
53 | @bgthread.kill | |
54 | end | |
55 | ||
56 | def read_and_execute | |
57 | command = read_line | |
58 | ||
59 | case command | |
60 | when "?", "help" then help | |
61 | when "quit", "exit" then | |
62 | puts "exiting" | |
63 | return false | |
64 | when /^set -(\w)\s*(\S+)/ | |
65 | set_option($1, $2) | |
66 | when /^(?:(with|on)\s*(\S+))?\s*(\S.*)?/i | |
67 | process_command($1, $2, $3) | |
68 | else | |
69 | raise "eh?" | |
70 | end | |
71 | ||
72 | return true | |
73 | end | |
74 | ||
75 | private | |
76 | ||
77 | # Present the prompt and read a single line from the console. It also | |
78 | # detects ^D and returns "exit" in that case. Adds the input to the | |
79 | # history, unless the input is empty. Loops repeatedly until a non-empty | |
80 | # line is input. | |
81 | def read_line | |
82 | loop do | |
83 | command = reader.readline("cap> ") | |
84 | ||
85 | if command.nil? | |
86 | command = "exit" | |
87 | puts(command) | |
88 | else | |
89 | command.strip! | |
90 | end | |
91 | ||
92 | unless command.empty? | |
93 | reader::HISTORY << command | |
94 | return command | |
95 | end | |
96 | end | |
97 | end | |
98 | ||
99 | # Display a verbose help message. | |
100 | def help | |
101 | puts <<-HELP | |
102 | --- HELP! --------------------------------------------------- | |
103 | "Get me out of this thing. I just want to quit." | |
104 | -> Easy enough. Just type "exit", or "quit". Or press ctrl-D. | |
105 | ||
106 | "I want to execute a command on all servers." | |
107 | -> Just type the command, and press enter. It will be passed, | |
108 | verbatim, to all defined servers. | |
109 | ||
110 | "What if I only want it to execute on a subset of them?" | |
111 | -> No problem, just specify the list of servers, separated by | |
112 | commas, before the command, with the `on' keyword: | |
113 | ||
114 | cap> on app1.foo.com,app2.foo.com echo ping | |
115 | ||
116 | "Nice, but can I specify the servers by role?" | |
117 | -> You sure can. Just use the `with' keyword, followed by the | |
118 | comma-delimited list of role names: | |
119 | ||
120 | cap> with app,db echo ping | |
121 | ||
122 | "Can I execute a Capistrano task from within this shell?" | |
123 | -> Yup. Just prefix the task with an exclamation mark: | |
124 | ||
125 | cap> !deploy | |
126 | HELP | |
127 | end | |
128 | ||
129 | # Determine which servers the given task requires a connection to, and | |
130 | # establish connections to them if necessary. Return the list of | |
131 | # servers (names). | |
132 | def connect(task) | |
133 | servers = configuration.find_servers_for_task(task) | |
134 | needing_connections = servers - configuration.sessions.keys | |
135 | unless needing_connections.empty? | |
136 | puts "[establishing connection(s) to #{needing_connections.join(', ')}]" | |
137 | configuration.establish_connections_to(needing_connections) | |
138 | end | |
139 | servers | |
140 | end | |
141 | ||
142 | # Execute the given command. If the command is prefixed by an exclamation | |
143 | # mark, it is assumed to refer to another capistrano task, which will | |
144 | # be invoked. Otherwise, it is executed as a command on all associated | |
145 | # servers. | |
146 | def exec(command) | |
147 | @mutex.synchronize do | |
148 | if command[0] == ?! | |
149 | exec_tasks(command[1..-1].split) | |
150 | else | |
151 | servers = connect(configuration.current_task) | |
152 | exec_command(command, servers) | |
153 | end | |
154 | end | |
155 | ensure | |
156 | STDOUT.flush | |
157 | end | |
158 | ||
159 | # Given an array of task names, invoke them in sequence. | |
160 | def exec_tasks(list) | |
161 | list.each do |task_name| | |
162 | task = configuration.find_task(task_name) | |
163 | raise Capistrano::NoSuchTaskError, "no such task `#{task_name}'" unless task | |
164 | connect(task) | |
165 | configuration.execute_task(task) | |
166 | end | |
167 | rescue Capistrano::NoMatchingServersError, Capistrano::NoSuchTaskError => error | |
168 | warn "error: #{error.message}" | |
169 | end | |
170 | ||
171 | # Execute a command on the given list of servers. | |
172 | def exec_command(command, servers) | |
173 | command = command.gsub(/\bsudo\b/, "sudo -p '#{configuration.sudo_prompt}'") | |
174 | processor = configuration.sudo_behavior_callback(Configuration.default_io_proc) | |
175 | sessions = servers.map { |server| configuration.sessions[server] } | |
176 | options = configuration.add_default_command_options({}) | |
177 | cmd = Command.new(command, sessions, options.merge(:logger => configuration.logger), &processor) | |
178 | previous = trap("INT") { cmd.stop! } | |
179 | cmd.process! | |
180 | rescue Capistrano::Error => error | |
181 | warn "error: #{error.message}" | |
182 | ensure | |
183 | trap("INT", previous) | |
184 | end | |
185 | ||
186 | # Return the object that will be used to query input from the console. | |
187 | # The returned object will quack (more or less) like Readline. | |
188 | def reader | |
189 | @reader ||= begin | |
190 | require 'readline' | |
191 | Readline | |
192 | rescue LoadError | |
193 | ReadlineFallback | |
194 | end | |
195 | end | |
196 | ||
197 | # Prepare every little thing for the shell. Starts the background | |
198 | # thread and generally gets things ready for the REPL. | |
199 | def setup | |
200 | configuration.logger.level = Capistrano::Logger::INFO | |
201 | ||
202 | @mutex = Mutex.new | |
203 | @bgthread = Thread.new do | |
204 | loop do | |
205 | @mutex.synchronize { process_iteration(0.1) } | |
206 | end | |
207 | end | |
208 | end | |
209 | ||
210 | # Set the given option to +value+. | |
211 | def set_option(opt, value) | |
212 | case opt | |
213 | when "v" then | |
214 | puts "setting log verbosity to #{value.to_i}" | |
215 | configuration.logger.level = value.to_i | |
216 | when "o" then | |
217 | case value | |
218 | when "vi" then | |
219 | puts "using vi edit mode" | |
220 | reader.vi_editing_mode | |
221 | when "emacs" then | |
222 | puts "using emacs edit mode" | |
223 | reader.emacs_editing_mode | |
224 | else | |
225 | puts "unknown -o option #{value.inspect}" | |
226 | end | |
227 | else | |
228 | puts "unknown setting #{opt.inspect}" | |
229 | end | |
230 | end | |
231 | ||
232 | # Process a command. Interprets the scope_type (must be nil, "with", or | |
233 | # "on") and the command. If no command is given, then the scope is made | |
234 | # effective for all subsequent commands. If the scope value is "all", | |
235 | # then the scope is unrestricted. | |
236 | def process_command(scope_type, scope_value, command) | |
237 | env_var = case scope_type | |
238 | when "with" then "ROLES" | |
239 | when "on" then "HOSTS" | |
240 | end | |
241 | ||
242 | old_var, ENV[env_var] = ENV[env_var], (scope_value == "all" ? nil : scope_value) if env_var | |
243 | if command | |
244 | begin | |
245 | exec(command) | |
246 | ensure | |
247 | ENV[env_var] = old_var if env_var | |
248 | end | |
249 | else | |
250 | puts "scoping #{scope_type} #{scope_value}" | |
251 | end | |
252 | end | |
253 | end | |
254 | ||
255 | # All open sessions, needed to satisfy the Command::Processable include | |
256 | def sessions | |
257 | configuration.sessions.values | |
258 | end | |
259 | end |
0 | require 'net/ssh' | |
1 | ||
2 | module Capistrano | |
3 | # A helper class for dealing with SSH connections. | |
4 | class SSH | |
5 | # Patch an accessor onto an SSH connection so that we can record the server | |
6 | # definition object that defines the connection. This is useful because | |
7 | # the gateway returns connections whose "host" is 127.0.0.1, instead of | |
8 | # the host on the other side of the tunnel. | |
9 | module Server #:nodoc: | |
10 | def self.apply_to(connection, server) | |
11 | connection.extend(Server) | |
12 | connection.xserver = server | |
13 | connection | |
14 | end | |
15 | ||
16 | attr_accessor :xserver | |
17 | end | |
18 | ||
19 | # An abstraction to make it possible to connect to the server via public key | |
20 | # without prompting for the password. If the public key authentication fails | |
21 | # this will fall back to password authentication. | |
22 | # | |
23 | # +server+ must be an instance of ServerDefinition. | |
24 | # | |
25 | # If a block is given, the new session is yielded to it, otherwise the new | |
26 | # session is returned. | |
27 | # | |
28 | # If an :ssh_options key exists in +options+, it is passed to the Net::SSH | |
29 | # constructor. Values in +options+ are then merged into it, and any | |
30 | # connection information in +server+ is added last, so that +server+ info | |
31 | # takes precedence over +options+, which takes precendence over ssh_options. | |
32 | def self.connect(server, options={}) | |
33 | connection_strategy(server, options) do |host, user, connection_options| | |
34 | connection = Net::SSH.start(host, user, connection_options) | |
35 | Server.apply_to(connection, server) | |
36 | end | |
37 | end | |
38 | ||
39 | # Abstracts the logic for establishing an SSH connection (which includes | |
40 | # testing for connection failures and retrying with a password, and so forth, | |
41 | # mostly made complicated because of the fact that some of these variables | |
42 | # might be lazily evaluated and try to do something like prompt the user, | |
43 | # which should only happen when absolutely necessary. | |
44 | # | |
45 | # This will yield the hostname, username, and a hash of connection options | |
46 | # to the given block, which should return a new connection. | |
47 | def self.connection_strategy(server, options={}, &block) | |
48 | methods = [ %w(publickey hostbased), %w(password keyboard-interactive) ] | |
49 | password_value = nil | |
50 | ||
51 | # construct the hash of ssh options that should be passed more-or-less | |
52 | # directly to Net::SSH. This will be the general ssh options, merged with | |
53 | # the server-specific ssh-options. | |
54 | ssh_options = (options[:ssh_options] || {}).merge(server.options[:ssh_options] || {}) | |
55 | ||
56 | # load any SSH configuration files that were specified in the SSH options. This | |
57 | # will load from ~/.ssh/config and /etc/ssh_config by default (see Net::SSH | |
58 | # for details). Merge the explicitly given ssh_options over the top of the info | |
59 | # from the config file. | |
60 | ssh_options = Net::SSH.configuration_for(server.host, ssh_options.fetch(:config, true)).merge(ssh_options) | |
61 | ||
62 | # Once we've loaded the config, we don't need Net::SSH to do it again. | |
63 | ssh_options[:config] = false | |
64 | ||
65 | ssh_options[:verbose] = :debug if options[:verbose] && options[:verbose] > 0 | |
66 | ||
67 | user = server.user || options[:user] || ssh_options[:username] || | |
68 | ssh_options[:user] || ServerDefinition.default_user | |
69 | port = server.port || options[:port] || ssh_options[:port] | |
70 | ||
71 | # the .ssh/config file might have changed the host-name on us | |
72 | host = ssh_options.fetch(:host_name, server.host) | |
73 | ||
74 | ssh_options[:port] = port if port | |
75 | ||
76 | # delete these, since we've determined which username to use by this point | |
77 | ssh_options.delete(:username) | |
78 | ssh_options.delete(:user) | |
79 | ||
80 | begin | |
81 | connection_options = ssh_options.merge( | |
82 | :password => password_value, | |
83 | :auth_methods => ssh_options[:auth_methods] || methods.shift | |
84 | ) | |
85 | ||
86 | yield host, user, connection_options | |
87 | rescue Net::SSH::AuthenticationFailed | |
88 | raise if methods.empty? || ssh_options[:auth_methods] | |
89 | password_value = options[:password] | |
90 | retry | |
91 | end | |
92 | end | |
93 | end | |
94 | end |
0 | load File.expand_path("../tasks/svn.rake", __FILE__) | |
1 | ||
2 | require 'capistrano/scm' | |
3 | ||
4 | class Capistrano::Svn < Capistrano::SCM | |
5 | ||
6 | # execute svn in context with arguments | |
7 | def svn(*args) | |
8 | args.unshift(:svn) | |
9 | context.execute *args | |
10 | end | |
11 | ||
12 | module DefaultStrategy | |
13 | def test | |
14 | test! " [ -d #{repo_path}/.svn ] " | |
15 | end | |
16 | ||
17 | def check | |
18 | test! :svn, :info, repo_url | |
19 | end | |
20 | ||
21 | def clone | |
22 | svn :checkout, repo_url, repo_path | |
23 | end | |
24 | ||
25 | def update | |
26 | svn :update | |
27 | end | |
28 | ||
29 | def release | |
30 | svn :export, '.', release_path | |
31 | end | |
32 | ||
33 | def fetch_revision | |
34 | context.capture(:svn, "log -r HEAD -q | tail -n 2 | head -n 1 | sed s/\ \|.*/''/") | |
35 | end | |
36 | end | |
37 | end |
0 | require 'capistrano/server_definition' | |
1 | ||
2 | module Capistrano | |
3 | ||
4 | class TaskDefinition | |
5 | ||
6 | attr_reader :name, :namespace, :options, :body, :desc, :on_error, :max_hosts | |
7 | ||
8 | def initialize(name, namespace, options={}, &block) | |
9 | @name, @namespace, @options = name, namespace, options | |
10 | @desc = @options.delete(:desc) | |
11 | @on_error = options.delete(:on_error) | |
12 | @max_hosts = options[:max_hosts] && options[:max_hosts].to_i | |
13 | @body = block or raise ArgumentError, "a task requires a block" | |
14 | @servers = nil | |
15 | end | |
16 | ||
17 | # Returns the task's fully-qualified name, including the namespace | |
18 | def fully_qualified_name | |
19 | @fully_qualified_name ||= begin | |
20 | if namespace.default_task == self | |
21 | namespace.fully_qualified_name | |
22 | else | |
23 | [namespace.fully_qualified_name, name].compact.join(":") | |
24 | end | |
25 | end | |
26 | end | |
27 | ||
28 | def name=(value) | |
29 | raise ArgumentError, "expected a valid task name" if !value.respond_to?(:to_sym) | |
30 | @name = value.to_sym | |
31 | end | |
32 | ||
33 | # Returns the description for this task, with newlines collapsed and | |
34 | # whitespace stripped. Returns the empty string if there is no | |
35 | # description for this task. | |
36 | def description(rebuild=false) | |
37 | @description = nil if rebuild | |
38 | @description ||= begin | |
39 | description = @desc || "" | |
40 | ||
41 | indentation = description[/\A\s+/] | |
42 | if indentation | |
43 | reformatted_description = "" | |
44 | description.strip.each_line do |line| | |
45 | line = line.chomp.sub(/^#{indentation}/, "") | |
46 | line = line.gsub(/#{indentation}\s*/, " ") if line[/^\S/] | |
47 | reformatted_description << line << "\n" | |
48 | end | |
49 | description = reformatted_description | |
50 | end | |
51 | ||
52 | description.strip.gsub(/\r\n/, "\n") | |
53 | end | |
54 | end | |
55 | ||
56 | # Returns the first sentence of the full description. If +max_length+ is | |
57 | # given, the result will be truncated if it is longer than +max_length+, | |
58 | # and an ellipsis appended. | |
59 | def brief_description(max_length=nil) | |
60 | brief = description[/^.*?\.(?=\s|$)/] || description | |
61 | ||
62 | if max_length && brief.length > max_length | |
63 | brief = brief[0,max_length-3] + "..." | |
64 | end | |
65 | ||
66 | brief | |
67 | end | |
68 | ||
69 | # Indicates whether the task wants to continue, even if a server has failed | |
70 | # previously | |
71 | def continue_on_error? | |
72 | @on_error == :continue | |
73 | end | |
74 | ||
75 | end | |
76 | end |
0 | desc 'Execute remote commands' | |
1 | task :console do | |
2 | stage = fetch(:stage) | |
3 | puts I18n.t('console.welcome', scope: :capistrano, stage: stage) | |
4 | loop do | |
5 | print "#{stage}> " | |
6 | ||
7 | if input = $stdin.gets | |
8 | command = input.chomp | |
9 | else | |
10 | command = 'exit' | |
11 | end | |
12 | ||
13 | next if command.empty? | |
14 | ||
15 | if %w{quit exit q}.include? command | |
16 | puts t('console.bye') | |
17 | break | |
18 | else | |
19 | begin | |
20 | on roles :all do | |
21 | execute command | |
22 | end | |
23 | rescue => e | |
24 | puts e | |
25 | end | |
26 | end | |
27 | end | |
28 | end |
0 | namespace :deploy do | |
1 | ||
2 | task :starting do | |
3 | invoke 'deploy:check' | |
4 | invoke 'deploy:set_previous_revision' | |
5 | end | |
6 | ||
7 | task :updating => :new_release_path do | |
8 | invoke "#{scm}:create_release" | |
9 | invoke "deploy:set_current_revision" | |
10 | invoke 'deploy:symlink:shared' | |
11 | end | |
12 | ||
13 | task :reverting do | |
14 | invoke 'deploy:revert_release' | |
15 | end | |
16 | ||
17 | task :publishing do | |
18 | invoke 'deploy:symlink:release' | |
19 | end | |
20 | ||
21 | task :finishing do | |
22 | invoke 'deploy:cleanup' | |
23 | end | |
24 | ||
25 | task :finishing_rollback do | |
26 | invoke 'deploy:cleanup_rollback' | |
27 | end | |
28 | ||
29 | task :finished do | |
30 | invoke 'deploy:log_revision' | |
31 | end | |
32 | ||
33 | desc 'Check required files and directories exist' | |
34 | task :check do | |
35 | invoke "#{scm}:check" | |
36 | invoke 'deploy:check:directories' | |
37 | invoke 'deploy:check:linked_dirs' | |
38 | invoke 'deploy:check:make_linked_dirs' | |
39 | invoke 'deploy:check:linked_files' | |
40 | end | |
41 | ||
42 | namespace :check do | |
43 | desc 'Check shared and release directories exist' | |
44 | task :directories do | |
45 | on release_roles :all do | |
46 | execute :mkdir, '-pv', shared_path, releases_path | |
47 | end | |
48 | end | |
49 | ||
50 | desc 'Check directories to be linked exist in shared' | |
51 | task :linked_dirs do | |
52 | next unless any? :linked_dirs | |
53 | on release_roles :all do | |
54 | execute :mkdir, '-pv', linked_dirs(shared_path) | |
55 | end | |
56 | end | |
57 | ||
58 | desc 'Check directories of files to be linked exist in shared' | |
59 | task :make_linked_dirs do | |
60 | next unless any? :linked_files | |
61 | on release_roles :all do |host| | |
62 | execute :mkdir, '-pv', linked_file_dirs(shared_path) | |
63 | end | |
64 | end | |
65 | ||
66 | desc 'Check files to be linked exist in shared' | |
67 | task :linked_files do | |
68 | next unless any? :linked_files | |
69 | on release_roles :all do |host| | |
70 | linked_files(shared_path).each do |file| | |
71 | unless test "[ -f #{file} ]" | |
72 | error t(:linked_file_does_not_exist, file: file, host: host) | |
73 | exit 1 | |
74 | end | |
75 | end | |
76 | end | |
77 | end | |
78 | end | |
79 | ||
80 | namespace :symlink do | |
81 | desc 'Symlink release to current' | |
82 | task :release do | |
83 | on release_roles :all do | |
84 | execute :rm, '-rf', current_path | |
85 | execute :ln, '-s', release_path, current_path | |
86 | end | |
87 | end | |
88 | ||
89 | desc 'Symlink files and directories from shared to release' | |
90 | task :shared do | |
91 | invoke 'deploy:symlink:linked_files' | |
92 | invoke 'deploy:symlink:linked_dirs' | |
93 | end | |
94 | ||
95 | desc 'Symlink linked directories' | |
96 | task :linked_dirs do | |
97 | next unless any? :linked_dirs | |
98 | on release_roles :all do | |
99 | execute :mkdir, '-pv', linked_dir_parents(release_path) | |
100 | ||
101 | fetch(:linked_dirs).each do |dir| | |
102 | target = release_path.join(dir) | |
103 | source = shared_path.join(dir) | |
104 | unless test "[ -L #{target} ]" | |
105 | if test "[ -d #{target} ]" | |
106 | execute :rm, '-rf', target | |
107 | end | |
108 | execute :ln, '-s', source, target | |
109 | end | |
110 | end | |
111 | end | |
112 | end | |
113 | ||
114 | desc 'Symlink linked files' | |
115 | task :linked_files do | |
116 | next unless any? :linked_files | |
117 | on release_roles :all do | |
118 | execute :mkdir, '-pv', linked_file_dirs(release_path) | |
119 | ||
120 | fetch(:linked_files).each do |file| | |
121 | target = release_path.join(file) | |
122 | source = shared_path.join(file) | |
123 | unless test "[ -L #{target} ]" | |
124 | if test "[ -f #{target} ]" | |
125 | execute :rm, target | |
126 | end | |
127 | execute :ln, '-s', source, target | |
128 | end | |
129 | end | |
130 | end | |
131 | end | |
132 | end | |
133 | ||
134 | desc 'Clean up old releases' | |
135 | task :cleanup do | |
136 | on release_roles :all do |host| | |
137 | releases = capture(:ls, '-x', releases_path).split | |
138 | if releases.count >= fetch(:keep_releases) | |
139 | info t(:keeping_releases, host: host.to_s, keep_releases: fetch(:keep_releases), releases: releases.count) | |
140 | directories = (releases - releases.last(fetch(:keep_releases))) | |
141 | if directories.any? | |
142 | directories_str = directories.map do |release| | |
143 | releases_path.join(release) | |
144 | end.join(" ") | |
145 | execute :rm, '-rf', directories_str | |
146 | else | |
147 | info t(:no_old_releases, host: host.to_s, keep_releases: fetch(:keep_releases)) | |
148 | end | |
149 | end | |
150 | end | |
151 | end | |
152 | ||
153 | desc 'Remove and archive rolled-back release.' | |
154 | task :cleanup_rollback do | |
155 | on release_roles(:all) do | |
156 | last_release = capture(:ls, '-xr', releases_path).split.first | |
157 | last_release_path = releases_path.join(last_release) | |
158 | if test "[ `readlink #{current_path}` != #{last_release_path} ]" | |
159 | execute :tar, '-czf', | |
160 | deploy_path.join("rolled-back-release-#{last_release}.tar.gz"), | |
161 | last_release_path | |
162 | execute :rm, '-rf', last_release_path | |
163 | else | |
164 | debug 'Last release is the current release, skip cleanup_rollback.' | |
165 | end | |
166 | end | |
167 | end | |
168 | ||
169 | desc 'Log details of the deploy' | |
170 | task :log_revision do | |
171 | on release_roles(:all) do | |
172 | within releases_path do | |
173 | execute %{echo "#{revision_log_message}" >> #{revision_log}} | |
174 | end | |
175 | end | |
176 | end | |
177 | ||
178 | desc 'Revert to previous release timestamp' | |
179 | task :revert_release => :rollback_release_path do | |
180 | on release_roles(:all) do | |
181 | set(:revision_log_message, rollback_log_message) | |
182 | end | |
183 | end | |
184 | ||
185 | task :new_release_path do | |
186 | set_release_path | |
187 | end | |
188 | ||
189 | task :rollback_release_path do | |
190 | on release_roles(:all) do | |
191 | releases = capture(:ls, '-xr', releases_path).split | |
192 | if releases.count < 2 | |
193 | error t(:cannot_rollback) | |
194 | exit 1 | |
195 | end | |
196 | last_release = releases[1] | |
197 | set_release_path(last_release) | |
198 | set(:rollback_timestamp, last_release) | |
199 | end | |
200 | end | |
201 | ||
202 | desc "Place a REVISION file with the current revision SHA in the current release path" | |
203 | task :set_current_revision do | |
204 | invoke "#{scm}:set_current_revision" | |
205 | on release_roles(:all) do | |
206 | within release_path do | |
207 | execute :echo, "\"#{fetch(:current_revision)}\" >> REVISION" | |
208 | end | |
209 | end | |
210 | end | |
211 | ||
212 | task :set_previous_revision do | |
213 | on release_roles(:all) do | |
214 | target = release_path.join('REVISION') | |
215 | if test "[ -f #{target} ]" | |
216 | set(:previous_revision, capture(:cat, target, '2>/dev/null')) | |
217 | end | |
218 | end | |
219 | end | |
220 | ||
221 | task :restart | |
222 | task :failed | |
223 | ||
224 | end |
0 | namespace :deploy do | |
1 | ||
2 | desc 'Start a deployment, make sure server(s) ready.' | |
3 | task :starting do | |
4 | end | |
5 | ||
6 | desc 'Started' | |
7 | task :started do | |
8 | end | |
9 | ||
10 | desc 'Update server(s) by setting up a new release.' | |
11 | task :updating do | |
12 | end | |
13 | ||
14 | desc 'Updated' | |
15 | task :updated do | |
16 | end | |
17 | ||
18 | desc 'Revert server(s) to previous release.' | |
19 | task :reverting do | |
20 | end | |
21 | ||
22 | desc 'Reverted' | |
23 | task :reverted do | |
24 | end | |
25 | ||
26 | desc 'Publish the release.' | |
27 | task :publishing do | |
28 | end | |
29 | ||
30 | desc 'Published' | |
31 | task :published do | |
32 | end | |
33 | ||
34 | desc 'Finish the deployment, clean up server(s).' | |
35 | task :finishing do | |
36 | end | |
37 | ||
38 | desc 'Finish the rollback, clean up server(s).' | |
39 | task :finishing_rollback do | |
40 | end | |
41 | ||
42 | desc 'Finished' | |
43 | task :finished do | |
44 | end | |
45 | ||
46 | desc 'Rollback to previous release.' | |
47 | task :rollback do | |
48 | %w{ starting started | |
49 | reverting reverted | |
50 | publishing published | |
51 | finishing_rollback finished }.each do |task| | |
52 | invoke "deploy:#{task}" | |
53 | end | |
54 | end | |
55 | end | |
56 | ||
57 | desc 'Deploy a new release.' | |
58 | task :deploy do | |
59 | set(:deploying, true) | |
60 | %w{ starting started | |
61 | updating updated | |
62 | publishing published | |
63 | finishing finished }.each do |task| | |
64 | invoke "deploy:#{task}" | |
65 | end | |
66 | end | |
67 | task default: :deploy |
0 | namespace :git do | |
1 | ||
2 | def strategy | |
3 | @strategy ||= Capistrano::Git.new(self, fetch(:git_strategy, Capistrano::Git::DefaultStrategy)) | |
4 | end | |
5 | ||
6 | set :git_environmental_variables, ->() { | |
7 | { | |
8 | git_askpass: "/bin/echo", | |
9 | git_ssh: "#{fetch(:tmp_dir)}/#{fetch(:application)}/git-ssh.sh" | |
10 | } | |
11 | } | |
12 | ||
13 | desc 'Upload the git wrapper script, this script guarantees that we can script git without getting an interactive prompt' | |
14 | task :wrapper do | |
15 | on release_roles :all do | |
16 | execute :mkdir, "-p", "#{fetch(:tmp_dir)}/#{fetch(:application)}/" | |
17 | upload! StringIO.new("#!/bin/sh -e\nexec /usr/bin/ssh -o PasswordAuthentication=no -o StrictHostKeyChecking=no \"$@\"\n"), "#{fetch(:tmp_dir)}/#{fetch(:application)}/git-ssh.sh" | |
18 | execute :chmod, "+x", "#{fetch(:tmp_dir)}/#{fetch(:application)}/git-ssh.sh" | |
19 | end | |
20 | end | |
21 | ||
22 | desc 'Check that the repository is reachable' | |
23 | task check: :'git:wrapper' do | |
24 | fetch(:branch) | |
25 | on release_roles :all do | |
26 | with fetch(:git_environmental_variables) do | |
27 | exit 1 unless strategy.check | |
28 | end | |
29 | end | |
30 | end | |
31 | ||
32 | desc 'Clone the repo to the cache' | |
33 | task clone: :'git:wrapper' do | |
34 | on release_roles :all do | |
35 | if strategy.test | |
36 | info t(:mirror_exists, at: repo_path) | |
37 | else | |
38 | within deploy_path do | |
39 | with fetch(:git_environmental_variables) do | |
40 | strategy.clone | |
41 | end | |
42 | end | |
43 | end | |
44 | end | |
45 | end | |
46 | ||
47 | desc 'Update the repo mirror to reflect the origin state' | |
48 | task update: :'git:clone' do | |
49 | on release_roles :all do | |
50 | within repo_path do | |
51 | with fetch(:git_environmental_variables) do | |
52 | strategy.update | |
53 | end | |
54 | end | |
55 | end | |
56 | end | |
57 | ||
58 | desc 'Copy repo to releases' | |
59 | task create_release: :'git:update' do | |
60 | on release_roles :all do | |
61 | with fetch(:git_environmental_variables) do | |
62 | within repo_path do | |
63 | execute :mkdir, '-p', release_path | |
64 | strategy.release | |
65 | end | |
66 | end | |
67 | end | |
68 | end | |
69 | ||
70 | desc 'Determine the revision that will be deployed' | |
71 | task :set_current_revision do | |
72 | on release_roles :all do | |
73 | within repo_path do | |
74 | with fetch(:git_environmental_variables) do | |
75 | set :current_revision, strategy.fetch_revision | |
76 | end | |
77 | end | |
78 | end | |
79 | end | |
80 | end |
0 | namespace :hg do | |
1 | def strategy | |
2 | @strategy ||= Capistrano::Hg.new(self, fetch(:hg_strategy, Capistrano::Hg::DefaultStrategy)) | |
3 | end | |
4 | ||
5 | desc 'Check that the repo is reachable' | |
6 | task :check do | |
7 | on release_roles :all do | |
8 | strategy.check | |
9 | end | |
10 | end | |
11 | ||
12 | desc 'Clone the repo to the cache' | |
13 | task :clone do | |
14 | on release_roles :all do | |
15 | if strategy.test | |
16 | info t(:mirror_exists, at: repo_path) | |
17 | else | |
18 | within deploy_path do | |
19 | strategy.clone | |
20 | end | |
21 | end | |
22 | end | |
23 | end | |
24 | ||
25 | desc 'Pull changes from the remote repo' | |
26 | task :update => :'hg:clone' do | |
27 | on release_roles :all do | |
28 | within repo_path do | |
29 | strategy.update | |
30 | end | |
31 | end | |
32 | end | |
33 | ||
34 | desc 'Copy repo to releases' | |
35 | task :create_release => :'hg:update' do | |
36 | on release_roles :all do | |
37 | within repo_path do | |
38 | strategy.release | |
39 | end | |
40 | end | |
41 | end | |
42 | ||
43 | desc 'Determine the revision that will be deployed' | |
44 | task :set_current_revision do | |
45 | on release_roles :all do | |
46 | within repo_path do | |
47 | set :current_revision, strategy.fetch_revision | |
48 | end | |
49 | end | |
50 | end | |
51 | end |
0 | require 'erb' | |
1 | require 'pathname' | |
2 | desc 'Install Capistrano, cap install STAGES=staging,production' | |
3 | task :install do | |
4 | envs = ENV['STAGES'] || 'staging,production' | |
5 | ||
6 | tasks_dir = Pathname.new('lib/capistrano/tasks') | |
7 | config_dir = Pathname.new('config') | |
8 | deploy_dir = config_dir.join('deploy') | |
9 | ||
10 | deploy_rb = File.expand_path("../../templates/deploy.rb.erb", __FILE__) | |
11 | stage_rb = File.expand_path("../../templates/stage.rb.erb", __FILE__) | |
12 | capfile = File.expand_path("../../templates/Capfile", __FILE__) | |
13 | ||
14 | mkdir_p deploy_dir | |
15 | ||
16 | template = File.read(deploy_rb) | |
17 | file = config_dir.join('deploy.rb') | |
18 | File.open(file, 'w+') do |f| | |
19 | f.write(ERB.new(template).result(binding)) | |
20 | puts I18n.t(:written_file, scope: :capistrano, file: file) | |
21 | end | |
22 | ||
23 | template = File.read(stage_rb) | |
24 | envs.split(',').each do |stage| | |
25 | file = deploy_dir.join("#{stage}.rb") | |
26 | File.open(file, 'w+') do |f| | |
27 | f.write(ERB.new(template).result(binding)) | |
28 | puts I18n.t(:written_file, scope: :capistrano, file: file) | |
29 | end | |
30 | end | |
31 | ||
32 | mkdir_p tasks_dir | |
33 | ||
34 | FileUtils.cp(capfile, 'Capfile') | |
35 | ||
36 | ||
37 | puts I18n.t :capified, scope: :capistrano | |
38 | end |
0 | namespace :svn do | |
1 | def strategy | |
2 | @strategy ||= Capistrano::Svn.new(self, fetch(:svn_strategy, Capistrano::Svn::DefaultStrategy)) | |
3 | end | |
4 | ||
5 | desc 'Check that the repo is reachable' | |
6 | task :check do | |
7 | on release_roles :all do | |
8 | strategy.check | |
9 | end | |
10 | end | |
11 | ||
12 | desc 'Clone the repo to the cache' | |
13 | task :clone do | |
14 | on release_roles :all do | |
15 | if strategy.test | |
16 | info t(:mirror_exists, at: repo_path) | |
17 | else | |
18 | within deploy_path do | |
19 | strategy.clone | |
20 | end | |
21 | end | |
22 | end | |
23 | end | |
24 | ||
25 | desc 'Pull changes from the remote repo' | |
26 | task :update => :'svn:clone' do | |
27 | on release_roles :all do | |
28 | within repo_path do | |
29 | strategy.update | |
30 | end | |
31 | end | |
32 | end | |
33 | ||
34 | desc 'Copy repo to releases' | |
35 | task :create_release => :'svn:update' do | |
36 | on release_roles :all do | |
37 | within repo_path do | |
38 | strategy.release | |
39 | end | |
40 | end | |
41 | end | |
42 | ||
43 | desc 'Determine the revision that will be deployed' | |
44 | task :set_current_revision do | |
45 | on release_roles :all do | |
46 | within repo_path do | |
47 | set :current_revision, strategy.fetch_revision | |
48 | end | |
49 | end | |
50 | end | |
51 | end |
0 | # Load DSL and Setup Up Stages | |
1 | require 'capistrano/setup' | |
2 | ||
3 | # Includes default deployment tasks | |
4 | require 'capistrano/deploy' | |
5 | ||
6 | # Includes tasks from other gems included in your Gemfile | |
7 | # | |
8 | # For documentation on these, see for example: | |
9 | # | |
10 | # https://github.com/capistrano/rvm | |
11 | # https://github.com/capistrano/rbenv | |
12 | # https://github.com/capistrano/chruby | |
13 | # https://github.com/capistrano/bundler | |
14 | # https://github.com/capistrano/rails | |
15 | # | |
16 | # require 'capistrano/rvm' | |
17 | # require 'capistrano/rbenv' | |
18 | # require 'capistrano/chruby' | |
19 | # require 'capistrano/bundler' | |
20 | # require 'capistrano/rails/assets' | |
21 | # require 'capistrano/rails/migrations' | |
22 | ||
23 | # Loads custom tasks from `lib/capistrano/tasks' if you have any defined. | |
24 | Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r } |
0 | # config valid only for Capistrano 3.1 | |
1 | lock '<%= Capistrano::VERSION %>' | |
2 | ||
3 | set :application, 'my_app_name' | |
4 | set :repo_url, 'git@example.com:me/my_repo.git' | |
5 | ||
6 | # Default branch is :master | |
7 | # ask :branch, proc { `git rev-parse --abbrev-ref HEAD`.chomp }.call | |
8 | ||
9 | # Default deploy_to directory is /var/www/my_app | |
10 | # set :deploy_to, '/var/www/my_app' | |
11 | ||
12 | # Default value for :scm is :git | |
13 | # set :scm, :git | |
14 | ||
15 | # Default value for :format is :pretty | |
16 | # set :format, :pretty | |
17 | ||
18 | # Default value for :log_level is :debug | |
19 | # set :log_level, :debug | |
20 | ||
21 | # Default value for :pty is false | |
22 | # set :pty, true | |
23 | ||
24 | # Default value for :linked_files is [] | |
25 | # set :linked_files, %w{config/database.yml} | |
26 | ||
27 | # Default value for linked_dirs is [] | |
28 | # set :linked_dirs, %w{bin log tmp/pids tmp/cache tmp/sockets vendor/bundle public/system} | |
29 | ||
30 | # Default value for default_env is {} | |
31 | # set :default_env, { path: "/opt/ruby/bin:$PATH" } | |
32 | ||
33 | # Default value for keep_releases is 5 | |
34 | # set :keep_releases, 5 | |
35 | ||
36 | namespace :deploy do | |
37 | ||
38 | desc 'Restart application' | |
39 | task :restart do | |
40 | on roles(:app), in: :sequence, wait: 5 do | |
41 | # Your restart mechanism here, for example: | |
42 | # execute :touch, release_path.join('tmp/restart.txt') | |
43 | end | |
44 | end | |
45 | ||
46 | after :publishing, :restart | |
47 | ||
48 | after :restart, :clear_cache do | |
49 | on roles(:web), in: :groups, limit: 3, wait: 10 do | |
50 | # Here we can do anything such as: | |
51 | # within release_path do | |
52 | # execute :rake, 'cache:clear' | |
53 | # end | |
54 | end | |
55 | end | |
56 | ||
57 | end |
0 | # Simple Role Syntax | |
1 | # ================== | |
2 | # Supports bulk-adding hosts to roles, the primary server in each group | |
3 | # is considered to be the first unless any hosts have the primary | |
4 | # property set. Don't declare `role :all`, it's a meta role. | |
5 | ||
6 | role :app, %w{deploy@example.com} | |
7 | role :web, %w{deploy@example.com} | |
8 | role :db, %w{deploy@example.com} | |
9 | ||
10 | ||
11 | # Extended Server Syntax | |
12 | # ====================== | |
13 | # This can be used to drop a more detailed server definition into the | |
14 | # server list. The second argument is a, or duck-types, Hash and is | |
15 | # used to set extended properties on the server. | |
16 | ||
17 | server 'example.com', user: 'deploy', roles: %w{web app}, my_property: :my_value | |
18 | ||
19 | ||
20 | # Custom SSH Options | |
21 | # ================== | |
22 | # You may pass any option but keep in mind that net/ssh understands a | |
23 | # limited set of options, consult[net/ssh documentation](http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start). | |
24 | # | |
25 | # Global options | |
26 | # -------------- | |
27 | # set :ssh_options, { | |
28 | # keys: %w(/home/rlisowski/.ssh/id_rsa), | |
29 | # forward_agent: false, | |
30 | # auth_methods: %w(password) | |
31 | # } | |
32 | # | |
33 | # And/or per server (overrides global) | |
34 | # ------------------------------------ | |
35 | # server 'example.com', | |
36 | # user: 'user_name', | |
37 | # roles: %w{web app}, | |
38 | # ssh_options: { | |
39 | # user: 'user_name', # overrides user setting above | |
40 | # keys: %w(/home/user_name/.ssh/id_rsa), | |
41 | # forward_agent: false, | |
42 | # auth_methods: %w(publickey password) | |
43 | # # password: 'please use keys' | |
44 | # } |
0 | require 'net/scp' | |
1 | require 'net/sftp' | |
2 | ||
3 | require 'capistrano/processable' | |
4 | ||
5 | module Capistrano | |
6 | class Transfer | |
7 | include Processable | |
8 | ||
9 | def self.process(direction, from, to, sessions, options={}, &block) | |
10 | new(direction, from, to, sessions, options, &block).process! | |
11 | end | |
12 | ||
13 | attr_reader :sessions | |
14 | attr_reader :options | |
15 | attr_reader :callback | |
16 | ||
17 | attr_reader :transport | |
18 | attr_reader :direction | |
19 | attr_reader :from | |
20 | attr_reader :to | |
21 | ||
22 | attr_reader :logger | |
23 | attr_reader :transfers | |
24 | ||
25 | def initialize(direction, from, to, sessions, options={}, &block) | |
26 | @direction = direction | |
27 | @from = from | |
28 | @to = to | |
29 | @sessions = sessions | |
30 | @options = options | |
31 | @callback = block | |
32 | ||
33 | @transport = options.fetch(:via, :sftp) | |
34 | @logger = options.delete(:logger) | |
35 | ||
36 | @session_map = {} | |
37 | ||
38 | prepare_transfers | |
39 | end | |
40 | ||
41 | def process! | |
42 | loop do | |
43 | begin | |
44 | break unless process_iteration { active? } | |
45 | rescue Exception => error | |
46 | if error.respond_to?(:session) | |
47 | handle_error(error) | |
48 | else | |
49 | raise | |
50 | end | |
51 | end | |
52 | end | |
53 | ||
54 | failed = transfers.select { |txfr| txfr[:failed] } | |
55 | if failed.any? | |
56 | hosts = failed.map { |txfr| txfr[:server] } | |
57 | errors = failed.map { |txfr| "#{txfr[:error]} (#{txfr[:error].message})" }.uniq.join(", ") | |
58 | error = TransferError.new("#{operation} via #{transport} failed on #{hosts.join(',')}: #{errors}") | |
59 | error.hosts = hosts | |
60 | ||
61 | logger.important(error.message) if logger | |
62 | raise error | |
63 | end | |
64 | ||
65 | logger.debug "#{transport} #{operation} complete" if logger | |
66 | self | |
67 | end | |
68 | ||
69 | def active? | |
70 | transfers.any? { |transfer| transfer.active? } | |
71 | end | |
72 | ||
73 | def operation | |
74 | "#{direction}load" | |
75 | end | |
76 | ||
77 | def sanitized_from | |
78 | if from.responds_to?(:read) | |
79 | "#<#{from.class}>" | |
80 | else | |
81 | from | |
82 | end | |
83 | end | |
84 | ||
85 | def sanitized_to | |
86 | if to.responds_to?(:read) | |
87 | "#<#{to.class}>" | |
88 | else | |
89 | to | |
90 | end | |
91 | end | |
92 | ||
93 | private | |
94 | ||
95 | def session_map | |
96 | @session_map | |
97 | end | |
98 | ||
99 | def prepare_transfers | |
100 | logger.info "#{transport} #{operation} #{from} -> #{to}" if logger | |
101 | ||
102 | @transfers = sessions.map do |session| | |
103 | session_from = normalize(from, session) | |
104 | session_to = normalize(to, session) | |
105 | ||
106 | session_map[session] = case transport | |
107 | when :sftp | |
108 | prepare_sftp_transfer(session_from, session_to, session) | |
109 | when :scp | |
110 | prepare_scp_transfer(session_from, session_to, session) | |
111 | else | |
112 | raise ArgumentError, "unsupported transport type: #{transport.inspect}" | |
113 | end | |
114 | end | |
115 | end | |
116 | ||
117 | def prepare_scp_transfer(from, to, session) | |
118 | real_callback = callback || Proc.new do |channel, name, sent, total| | |
119 | logger.trace "[#{channel[:host]}] #{name}" if logger && sent == 0 | |
120 | end | |
121 | ||
122 | channel = case direction | |
123 | when :up | |
124 | session.scp.upload(from, to, options, &real_callback) | |
125 | when :down | |
126 | session.scp.download(from, to, options, &real_callback) | |
127 | else | |
128 | raise ArgumentError, "unsupported transfer direction: #{direction.inspect}" | |
129 | end | |
130 | ||
131 | channel[:server] = session.xserver | |
132 | channel[:host] = session.xserver.host | |
133 | ||
134 | return channel | |
135 | end | |
136 | ||
137 | class SFTPTransferWrapper | |
138 | attr_reader :operation | |
139 | ||
140 | def initialize(session, &callback) | |
141 | session.sftp(false).connect do |sftp| | |
142 | @operation = callback.call(sftp) | |
143 | end | |
144 | end | |
145 | ||
146 | def active? | |
147 | @operation.nil? || @operation.active? | |
148 | end | |
149 | ||
150 | def [](key) | |
151 | @operation[key] | |
152 | end | |
153 | ||
154 | def []=(key, value) | |
155 | @operation[key] = value | |
156 | end | |
157 | ||
158 | def abort! | |
159 | @operation.abort! | |
160 | end | |
161 | end | |
162 | ||
163 | def prepare_sftp_transfer(from, to, session) | |
164 | SFTPTransferWrapper.new(session) do |sftp| | |
165 | real_callback = Proc.new do |event, op, *args| | |
166 | if callback | |
167 | callback.call(event, op, *args) | |
168 | elsif event == :open | |
169 | logger.trace "[#{op[:host]}] #{args[0].remote}" | |
170 | elsif event == :finish | |
171 | logger.trace "[#{op[:host]}] done" | |
172 | end | |
173 | end | |
174 | ||
175 | opts = options.dup | |
176 | opts[:properties] = (opts[:properties] || {}).merge( | |
177 | :server => session.xserver, | |
178 | :host => session.xserver.host) | |
179 | ||
180 | case direction | |
181 | when :up | |
182 | sftp.upload(from, to, opts, &real_callback) | |
183 | when :down | |
184 | sftp.download(from, to, opts, &real_callback) | |
185 | else | |
186 | raise ArgumentError, "unsupported transfer direction: #{direction.inspect}" | |
187 | end | |
188 | end | |
189 | end | |
190 | ||
191 | def normalize(argument, session) | |
192 | if argument.is_a?(String) | |
193 | argument.gsub(/\$CAPISTRANO:HOST\$/, session.xserver.host) | |
194 | elsif argument.respond_to?(:read) | |
195 | pos = argument.pos | |
196 | clone = StringIO.new(argument.read) | |
197 | clone.pos = argument.pos = pos | |
198 | clone | |
199 | else | |
200 | argument | |
201 | end | |
202 | end | |
203 | ||
204 | def handle_error(error) | |
205 | raise error if error.message.include?('expected a file to upload') | |
206 | ||
207 | transfer = session_map[error.session] | |
208 | transfer[:error] = error | |
209 | transfer[:failed] = true | |
210 | ||
211 | case transport | |
212 | when :sftp then transfer.abort! | |
213 | when :scp then transfer.close | |
214 | end | |
215 | end | |
216 | end | |
217 | end |
0 | require 'scanf' | |
1 | 0 | module Capistrano |
2 | ||
3 | class Version | |
4 | ||
5 | MAJOR = 2 | |
6 | MINOR = 12 | |
7 | PATCH = 0 | |
8 | ||
9 | def self.to_s | |
10 | "#{MAJOR}.#{MINOR}.#{PATCH}" | |
11 | end | |
12 | ||
13 | end | |
14 | ||
1 | VERSION = "3.2.0" | |
15 | 2 | end |
0 | module Capistrano | |
1 | class VersionValidator | |
2 | ||
3 | def initialize(version) | |
4 | @version = version | |
5 | end | |
6 | ||
7 | def verify | |
8 | if match? | |
9 | self | |
10 | else | |
11 | fail "Capfile locked at #{version}, but #{current_version} is loaded" | |
12 | end | |
13 | end | |
14 | ||
15 | private | |
16 | attr_reader :version | |
17 | ||
18 | ||
19 | def match? | |
20 | available =~ requested | |
21 | end | |
22 | ||
23 | def current_version | |
24 | VERSION | |
25 | end | |
26 | ||
27 | def available | |
28 | Gem::Dependency.new('cap', version) | |
29 | end | |
30 | ||
31 | def requested | |
32 | Gem::Dependency.new('cap', current_version) | |
33 | end | |
34 | ||
35 | end | |
36 | end |
0 | require 'capistrano/fix_rake_deprecated_dsl' | |
1 | ||
2 | require 'capistrano/configuration' | |
3 | require 'capistrano/extensions' | |
4 | require 'capistrano/ext/string' |
0 | 0 | --- !ruby/object:Gem::Specification |
1 | 1 | name: capistrano |
2 | 2 | version: !ruby/object:Gem::Version |
3 | version: 2.12.0 | |
4 | prerelease: | |
3 | version: 3.2.0 | |
5 | 4 | platform: ruby |
6 | 5 | authors: |
7 | - Jamis Buck | |
6 | - Tom Clements | |
8 | 7 | - Lee Hambley |
9 | 8 | autorequire: |
10 | 9 | bindir: bin |
11 | 10 | cert_chain: [] |
12 | date: 2012-04-13 00:00:00.000000000 Z | |
11 | date: 2014-04-15 00:00:00.000000000 Z | |
13 | 12 | dependencies: |
14 | 13 | - !ruby/object:Gem::Dependency |
15 | name: highline | |
16 | requirement: &70100200770260 !ruby/object:Gem::Requirement | |
17 | none: false | |
18 | requirements: | |
19 | - - ! '>=' | |
20 | - !ruby/object:Gem::Version | |
21 | version: '0' | |
14 | name: sshkit | |
15 | requirement: !ruby/object:Gem::Requirement | |
16 | requirements: | |
17 | - - ~> | |
18 | - !ruby/object:Gem::Version | |
19 | version: '1.3' | |
22 | 20 | type: :runtime |
23 | 21 | prerelease: false |
24 | version_requirements: *70100200770260 | |
25 | - !ruby/object:Gem::Dependency | |
26 | name: net-ssh | |
27 | requirement: &70100200769760 !ruby/object:Gem::Requirement | |
28 | none: false | |
29 | requirements: | |
30 | - - ! '>=' | |
31 | - !ruby/object:Gem::Version | |
32 | version: 2.0.14 | |
22 | version_requirements: !ruby/object:Gem::Requirement | |
23 | requirements: | |
24 | - - ~> | |
25 | - !ruby/object:Gem::Version | |
26 | version: '1.3' | |
27 | - !ruby/object:Gem::Dependency | |
28 | name: rake | |
29 | requirement: !ruby/object:Gem::Requirement | |
30 | requirements: | |
31 | - - '>=' | |
32 | - !ruby/object:Gem::Version | |
33 | version: 10.0.0 | |
33 | 34 | type: :runtime |
34 | 35 | prerelease: false |
35 | version_requirements: *70100200769760 | |
36 | - !ruby/object:Gem::Dependency | |
37 | name: net-sftp | |
38 | requirement: &70100200769280 !ruby/object:Gem::Requirement | |
39 | none: false | |
40 | requirements: | |
41 | - - ! '>=' | |
42 | - !ruby/object:Gem::Version | |
43 | version: 2.0.0 | |
36 | version_requirements: !ruby/object:Gem::Requirement | |
37 | requirements: | |
38 | - - '>=' | |
39 | - !ruby/object:Gem::Version | |
40 | version: 10.0.0 | |
41 | - !ruby/object:Gem::Dependency | |
42 | name: i18n | |
43 | requirement: !ruby/object:Gem::Requirement | |
44 | requirements: | |
45 | - - '>=' | |
46 | - !ruby/object:Gem::Version | |
47 | version: '0' | |
44 | 48 | type: :runtime |
45 | 49 | prerelease: false |
46 | version_requirements: *70100200769280 | |
47 | - !ruby/object:Gem::Dependency | |
48 | name: net-scp | |
49 | requirement: &70100200768800 !ruby/object:Gem::Requirement | |
50 | none: false | |
51 | requirements: | |
52 | - - ! '>=' | |
53 | - !ruby/object:Gem::Version | |
54 | version: 1.0.0 | |
55 | type: :runtime | |
56 | prerelease: false | |
57 | version_requirements: *70100200768800 | |
58 | - !ruby/object:Gem::Dependency | |
59 | name: net-ssh-gateway | |
60 | requirement: &70100200768320 !ruby/object:Gem::Requirement | |
61 | none: false | |
62 | requirements: | |
63 | - - ! '>=' | |
64 | - !ruby/object:Gem::Version | |
65 | version: 1.1.0 | |
66 | type: :runtime | |
67 | prerelease: false | |
68 | version_requirements: *70100200768320 | |
50 | version_requirements: !ruby/object:Gem::Requirement | |
51 | requirements: | |
52 | - - '>=' | |
53 | - !ruby/object:Gem::Version | |
54 | version: '0' | |
55 | - !ruby/object:Gem::Dependency | |
56 | name: rspec | |
57 | requirement: !ruby/object:Gem::Requirement | |
58 | requirements: | |
59 | - - '>=' | |
60 | - !ruby/object:Gem::Version | |
61 | version: '0' | |
62 | type: :development | |
63 | prerelease: false | |
64 | version_requirements: !ruby/object:Gem::Requirement | |
65 | requirements: | |
66 | - - '>=' | |
67 | - !ruby/object:Gem::Version | |
68 | version: '0' | |
69 | 69 | - !ruby/object:Gem::Dependency |
70 | 70 | name: mocha |
71 | requirement: &70100200767840 !ruby/object:Gem::Requirement | |
72 | none: false | |
73 | requirements: | |
74 | - - ! '>=' | |
71 | requirement: !ruby/object:Gem::Requirement | |
72 | requirements: | |
73 | - - '>=' | |
75 | 74 | - !ruby/object:Gem::Version |
76 | 75 | version: '0' |
77 | 76 | type: :development |
78 | 77 | prerelease: false |
79 | version_requirements: *70100200767840 | |
78 | version_requirements: !ruby/object:Gem::Requirement | |
79 | requirements: | |
80 | - - '>=' | |
81 | - !ruby/object:Gem::Version | |
82 | version: '0' | |
80 | 83 | description: Capistrano is a utility and framework for executing commands in parallel |
81 | 84 | on multiple remote machines, via SSH. |
82 | 85 | email: |
83 | - jamis@jamisbuck.org | |
86 | - seenmyfate@gmail.com | |
84 | 87 | - lee.hambley@gmail.com |
85 | 88 | executables: |
86 | 89 | - cap |
87 | 90 | - capify |
88 | 91 | extensions: [] |
89 | extra_rdoc_files: | |
90 | - README.mdown | |
92 | extra_rdoc_files: [] | |
91 | 93 | files: |
92 | 94 | - .gitignore |
93 | 95 | - .travis.yml |
94 | - CHANGELOG | |
96 | - CHANGELOG.md | |
97 | - CONTRIBUTING.md | |
95 | 98 | - Gemfile |
96 | - README.mdown | |
99 | - LICENSE.txt | |
100 | - README.md | |
97 | 101 | - Rakefile |
98 | 102 | - bin/cap |
99 | 103 | - bin/capify |
100 | 104 | - capistrano.gemspec |
105 | - features/configuration.feature | |
106 | - features/deploy.feature | |
107 | - features/deploy_failure.feature | |
108 | - features/installation.feature | |
109 | - features/remote_file_task.feature | |
110 | - features/step_definitions/assertions.rb | |
111 | - features/step_definitions/cap_commands.rb | |
112 | - features/step_definitions/setup.rb | |
113 | - features/support/env.rb | |
114 | - features/support/remote_command_helpers.rb | |
115 | - lib/Capfile | |
101 | 116 | - lib/capistrano.rb |
102 | - lib/capistrano/callback.rb | |
103 | - lib/capistrano/cli.rb | |
104 | - lib/capistrano/cli/execute.rb | |
105 | - lib/capistrano/cli/help.rb | |
106 | - lib/capistrano/cli/help.txt | |
107 | - lib/capistrano/cli/options.rb | |
108 | - lib/capistrano/cli/ui.rb | |
109 | - lib/capistrano/command.rb | |
117 | - lib/capistrano/all.rb | |
118 | - lib/capistrano/application.rb | |
110 | 119 | - lib/capistrano/configuration.rb |
111 | - lib/capistrano/configuration/actions/file_transfer.rb | |
112 | - lib/capistrano/configuration/actions/inspect.rb | |
113 | - lib/capistrano/configuration/actions/invocation.rb | |
114 | - lib/capistrano/configuration/alias_task.rb | |
115 | - lib/capistrano/configuration/callbacks.rb | |
116 | - lib/capistrano/configuration/connections.rb | |
117 | - lib/capistrano/configuration/execution.rb | |
118 | - lib/capistrano/configuration/loading.rb | |
119 | - lib/capistrano/configuration/namespaces.rb | |
120 | - lib/capistrano/configuration/roles.rb | |
120 | - lib/capistrano/configuration/question.rb | |
121 | - lib/capistrano/configuration/server.rb | |
121 | 122 | - lib/capistrano/configuration/servers.rb |
122 | - lib/capistrano/configuration/variables.rb | |
123 | - lib/capistrano/errors.rb | |
124 | - lib/capistrano/ext/multistage.rb | |
125 | - lib/capistrano/ext/string.rb | |
126 | - lib/capistrano/extensions.rb | |
127 | - lib/capistrano/fix_rake_deprecated_dsl.rb | |
128 | - lib/capistrano/logger.rb | |
129 | - lib/capistrano/processable.rb | |
130 | - lib/capistrano/recipes/compat.rb | |
131 | - lib/capistrano/recipes/deploy.rb | |
132 | - lib/capistrano/recipes/deploy/assets.rb | |
133 | - lib/capistrano/recipes/deploy/dependencies.rb | |
134 | - lib/capistrano/recipes/deploy/local_dependency.rb | |
135 | - lib/capistrano/recipes/deploy/remote_dependency.rb | |
136 | - lib/capistrano/recipes/deploy/scm.rb | |
137 | - lib/capistrano/recipes/deploy/scm/accurev.rb | |
138 | - lib/capistrano/recipes/deploy/scm/base.rb | |
139 | - lib/capistrano/recipes/deploy/scm/bzr.rb | |
140 | - lib/capistrano/recipes/deploy/scm/cvs.rb | |
141 | - lib/capistrano/recipes/deploy/scm/darcs.rb | |
142 | - lib/capistrano/recipes/deploy/scm/git.rb | |
143 | - lib/capistrano/recipes/deploy/scm/mercurial.rb | |
144 | - lib/capistrano/recipes/deploy/scm/none.rb | |
145 | - lib/capistrano/recipes/deploy/scm/perforce.rb | |
146 | - lib/capistrano/recipes/deploy/scm/subversion.rb | |
147 | - lib/capistrano/recipes/deploy/strategy.rb | |
148 | - lib/capistrano/recipes/deploy/strategy/base.rb | |
149 | - lib/capistrano/recipes/deploy/strategy/checkout.rb | |
150 | - lib/capistrano/recipes/deploy/strategy/copy.rb | |
151 | - lib/capistrano/recipes/deploy/strategy/export.rb | |
152 | - lib/capistrano/recipes/deploy/strategy/remote.rb | |
153 | - lib/capistrano/recipes/deploy/strategy/remote_cache.rb | |
154 | - lib/capistrano/recipes/deploy/strategy/unshared_remote_cache.rb | |
155 | - lib/capistrano/recipes/deploy/templates/maintenance.rhtml | |
156 | - lib/capistrano/recipes/standard.rb | |
157 | - lib/capistrano/recipes/templates/maintenance.rhtml | |
158 | - lib/capistrano/role.rb | |
159 | - lib/capistrano/server_definition.rb | |
160 | - lib/capistrano/shell.rb | |
161 | - lib/capistrano/ssh.rb | |
162 | - lib/capistrano/task_definition.rb | |
163 | - lib/capistrano/transfer.rb | |
123 | - lib/capistrano/configuration/servers/host_filter.rb | |
124 | - lib/capistrano/configuration/servers/role_filter.rb | |
125 | - lib/capistrano/console.rb | |
126 | - lib/capistrano/defaults.rb | |
127 | - lib/capistrano/deploy.rb | |
128 | - lib/capistrano/dotfile.rb | |
129 | - lib/capistrano/dsl.rb | |
130 | - lib/capistrano/dsl/env.rb | |
131 | - lib/capistrano/dsl/paths.rb | |
132 | - lib/capistrano/dsl/stages.rb | |
133 | - lib/capistrano/dsl/task_enhancements.rb | |
134 | - lib/capistrano/framework.rb | |
135 | - lib/capistrano/git.rb | |
136 | - lib/capistrano/hg.rb | |
137 | - lib/capistrano/i18n.rb | |
138 | - lib/capistrano/install.rb | |
139 | - lib/capistrano/scm.rb | |
140 | - lib/capistrano/setup.rb | |
141 | - lib/capistrano/svn.rb | |
142 | - lib/capistrano/tasks/console.rake | |
143 | - lib/capistrano/tasks/deploy.rake | |
144 | - lib/capistrano/tasks/framework.rake | |
145 | - lib/capistrano/tasks/git.rake | |
146 | - lib/capistrano/tasks/hg.rake | |
147 | - lib/capistrano/tasks/install.rake | |
148 | - lib/capistrano/tasks/svn.rake | |
149 | - lib/capistrano/templates/Capfile | |
150 | - lib/capistrano/templates/deploy.rb.erb | |
151 | - lib/capistrano/templates/stage.rb.erb | |
164 | 152 | - lib/capistrano/version.rb |
165 | - test/cli/execute_test.rb | |
166 | - test/cli/help_test.rb | |
167 | - test/cli/options_test.rb | |
168 | - test/cli/ui_test.rb | |
169 | - test/cli_test.rb | |
170 | - test/command_test.rb | |
171 | - test/configuration/actions/file_transfer_test.rb | |
172 | - test/configuration/actions/inspect_test.rb | |
173 | - test/configuration/actions/invocation_test.rb | |
174 | - test/configuration/alias_task_test.rb | |
175 | - test/configuration/callbacks_test.rb | |
176 | - test/configuration/connections_test.rb | |
177 | - test/configuration/execution_test.rb | |
178 | - test/configuration/loading_test.rb | |
179 | - test/configuration/namespace_dsl_test.rb | |
180 | - test/configuration/roles_test.rb | |
181 | - test/configuration/servers_test.rb | |
182 | - test/configuration/variables_test.rb | |
183 | - test/configuration_test.rb | |
184 | - test/deploy/local_dependency_test.rb | |
185 | - test/deploy/remote_dependency_test.rb | |
186 | - test/deploy/scm/accurev_test.rb | |
187 | - test/deploy/scm/base_test.rb | |
188 | - test/deploy/scm/bzr_test.rb | |
189 | - test/deploy/scm/darcs_test.rb | |
190 | - test/deploy/scm/git_test.rb | |
191 | - test/deploy/scm/mercurial_test.rb | |
192 | - test/deploy/scm/none_test.rb | |
193 | - test/deploy/scm/perforce_test.rb | |
194 | - test/deploy/scm/subversion_test.rb | |
195 | - test/deploy/strategy/copy_test.rb | |
196 | - test/extensions_test.rb | |
197 | - test/fixtures/cli_integration.rb | |
198 | - test/fixtures/config.rb | |
199 | - test/fixtures/custom.rb | |
200 | - test/logger_test.rb | |
201 | - test/recipes_test.rb | |
202 | - test/role_test.rb | |
203 | - test/server_definition_test.rb | |
204 | - test/shell_test.rb | |
205 | - test/ssh_test.rb | |
206 | - test/task_definition_test.rb | |
207 | - test/transfer_test.rb | |
208 | - test/utils.rb | |
209 | homepage: http://github.com/capistrano/capistrano | |
210 | licenses: [] | |
211 | post_install_message: | |
153 | - lib/capistrano/version_validator.rb | |
154 | - spec/integration/dsl_spec.rb | |
155 | - spec/integration_spec_helper.rb | |
156 | - spec/lib/capistrano/application_spec.rb | |
157 | - spec/lib/capistrano/configuration/question_spec.rb | |
158 | - spec/lib/capistrano/configuration/server_spec.rb | |
159 | - spec/lib/capistrano/configuration/servers/host_filter_spec.rb | |
160 | - spec/lib/capistrano/configuration/servers/role_filter_spec.rb | |
161 | - spec/lib/capistrano/configuration/servers_spec.rb | |
162 | - spec/lib/capistrano/configuration_spec.rb | |
163 | - spec/lib/capistrano/dsl/paths_spec.rb | |
164 | - spec/lib/capistrano/dsl_spec.rb | |
165 | - spec/lib/capistrano/git_spec.rb | |
166 | - spec/lib/capistrano/hg_spec.rb | |
167 | - spec/lib/capistrano/scm_spec.rb | |
168 | - spec/lib/capistrano/svn_spec.rb | |
169 | - spec/lib/capistrano/version_validator_spec.rb | |
170 | - spec/lib/capistrano_spec.rb | |
171 | - spec/spec_helper.rb | |
172 | - spec/support/.gitignore | |
173 | - spec/support/Vagrantfile | |
174 | - spec/support/matchers.rb | |
175 | - spec/support/tasks/database.rake | |
176 | - spec/support/tasks/fail.rake | |
177 | - spec/support/tasks/failed.rake | |
178 | - spec/support/test_app.rb | |
179 | homepage: http://capistranorb.com/ | |
180 | licenses: | |
181 | - MIT | |
182 | metadata: {} | |
183 | post_install_message: | | |
184 | Capistrano 3.1 has some breaking changes, like `deploy:restart` callback should be added manually to your deploy.rb. Please, check the CHANGELOG: http://goo.gl/SxB0lr | |
185 | ||
186 | If you're upgrading Capistrano from 2.x, we recommend to read the upgrade guide: http://goo.gl/4536kB | |
212 | 187 | rdoc_options: [] |
213 | 188 | require_paths: |
214 | 189 | - lib |
215 | 190 | required_ruby_version: !ruby/object:Gem::Requirement |
216 | none: false | |
217 | 191 | requirements: |
218 | - - ! '>=' | |
192 | - - '>=' | |
219 | 193 | - !ruby/object:Gem::Version |
220 | 194 | version: '0' |
221 | 195 | required_rubygems_version: !ruby/object:Gem::Requirement |
222 | none: false | |
223 | 196 | requirements: |
224 | - - ! '>=' | |
197 | - - '>=' | |
225 | 198 | - !ruby/object:Gem::Version |
226 | 199 | version: '0' |
227 | 200 | requirements: [] |
228 | 201 | rubyforge_project: |
229 | rubygems_version: 1.8.11 | |
202 | rubygems_version: 2.0.3 | |
230 | 203 | signing_key: |
231 | specification_version: 3 | |
204 | specification_version: 4 | |
232 | 205 | summary: Capistrano - Welcome to easy deployment with Ruby over SSH |
233 | 206 | test_files: |
234 | - test/cli/execute_test.rb | |
235 | - test/cli/help_test.rb | |
236 | - test/cli/options_test.rb | |
237 | - test/cli/ui_test.rb | |
238 | - test/cli_test.rb | |
239 | - test/command_test.rb | |
240 | - test/configuration/actions/file_transfer_test.rb | |
241 | - test/configuration/actions/inspect_test.rb | |
242 | - test/configuration/actions/invocation_test.rb | |
243 | - test/configuration/alias_task_test.rb | |
244 | - test/configuration/callbacks_test.rb | |
245 | - test/configuration/connections_test.rb | |
246 | - test/configuration/execution_test.rb | |
247 | - test/configuration/loading_test.rb | |
248 | - test/configuration/namespace_dsl_test.rb | |
249 | - test/configuration/roles_test.rb | |
250 | - test/configuration/servers_test.rb | |
251 | - test/configuration/variables_test.rb | |
252 | - test/configuration_test.rb | |
253 | - test/deploy/local_dependency_test.rb | |
254 | - test/deploy/remote_dependency_test.rb | |
255 | - test/deploy/scm/accurev_test.rb | |
256 | - test/deploy/scm/base_test.rb | |
257 | - test/deploy/scm/bzr_test.rb | |
258 | - test/deploy/scm/darcs_test.rb | |
259 | - test/deploy/scm/git_test.rb | |
260 | - test/deploy/scm/mercurial_test.rb | |
261 | - test/deploy/scm/none_test.rb | |
262 | - test/deploy/scm/perforce_test.rb | |
263 | - test/deploy/scm/subversion_test.rb | |
264 | - test/deploy/strategy/copy_test.rb | |
265 | - test/extensions_test.rb | |
266 | - test/fixtures/cli_integration.rb | |
267 | - test/fixtures/config.rb | |
268 | - test/fixtures/custom.rb | |
269 | - test/logger_test.rb | |
270 | - test/recipes_test.rb | |
271 | - test/role_test.rb | |
272 | - test/server_definition_test.rb | |
273 | - test/shell_test.rb | |
274 | - test/ssh_test.rb | |
275 | - test/task_definition_test.rb | |
276 | - test/transfer_test.rb | |
277 | - test/utils.rb | |
207 | - features/configuration.feature | |
208 | - features/deploy.feature | |
209 | - features/deploy_failure.feature | |
210 | - features/installation.feature | |
211 | - features/remote_file_task.feature | |
212 | - features/step_definitions/assertions.rb | |
213 | - features/step_definitions/cap_commands.rb | |
214 | - features/step_definitions/setup.rb | |
215 | - features/support/env.rb | |
216 | - features/support/remote_command_helpers.rb | |
217 | - spec/integration/dsl_spec.rb | |
218 | - spec/integration_spec_helper.rb | |
219 | - spec/lib/capistrano/application_spec.rb | |
220 | - spec/lib/capistrano/configuration/question_spec.rb | |
221 | - spec/lib/capistrano/configuration/server_spec.rb | |
222 | - spec/lib/capistrano/configuration/servers/host_filter_spec.rb | |
223 | - spec/lib/capistrano/configuration/servers/role_filter_spec.rb | |
224 | - spec/lib/capistrano/configuration/servers_spec.rb | |
225 | - spec/lib/capistrano/configuration_spec.rb | |
226 | - spec/lib/capistrano/dsl/paths_spec.rb | |
227 | - spec/lib/capistrano/dsl_spec.rb | |
228 | - spec/lib/capistrano/git_spec.rb | |
229 | - spec/lib/capistrano/hg_spec.rb | |
230 | - spec/lib/capistrano/scm_spec.rb | |
231 | - spec/lib/capistrano/svn_spec.rb | |
232 | - spec/lib/capistrano/version_validator_spec.rb | |
233 | - spec/lib/capistrano_spec.rb | |
234 | - spec/spec_helper.rb | |
235 | - spec/support/.gitignore | |
236 | - spec/support/Vagrantfile | |
237 | - spec/support/matchers.rb | |
238 | - spec/support/tasks/database.rake | |
239 | - spec/support/tasks/fail.rake | |
240 | - spec/support/tasks/failed.rake | |
241 | - spec/support/test_app.rb | |
242 | has_rdoc: |
0 | require 'spec_helper' | |
1 | ||
2 | describe Capistrano::DSL do | |
3 | ||
4 | let(:dsl) { Class.new.extend Capistrano::DSL } | |
5 | ||
6 | before do | |
7 | Capistrano::Configuration.reset! | |
8 | end | |
9 | ||
10 | describe 'setting and fetching hosts' do | |
11 | describe 'when defining a host using the `server` syntax' do | |
12 | before do | |
13 | dsl.server 'example1.com', roles: %w{web}, active: true | |
14 | dsl.server 'example2.com', roles: %w{web} | |
15 | dsl.server 'example3.com', roles: %w{app web}, active: true | |
16 | dsl.server 'example4.com', roles: %w{app}, primary: true | |
17 | dsl.server 'example5.com', roles: %w{db}, no_release: true | |
18 | end | |
19 | ||
20 | describe 'fetching all servers' do | |
21 | subject { dsl.roles(:all) } | |
22 | ||
23 | it 'returns all servers' do | |
24 | expect(subject.map(&:hostname)).to eq %w{example1.com example2.com example3.com example4.com example5.com} | |
25 | end | |
26 | end | |
27 | ||
28 | describe 'fetching all release servers' do | |
29 | ||
30 | context 'with no additional options' do | |
31 | subject { dsl.release_roles(:all) } | |
32 | ||
33 | it 'returns all release servers' do | |
34 | expect(subject.map(&:hostname)).to eq %w{example1.com example2.com example3.com example4.com} | |
35 | end | |
36 | end | |
37 | ||
38 | context 'with filter options' do | |
39 | subject { dsl.release_roles(:all, filter: :active) } | |
40 | ||
41 | it 'returns all release servers that match the filter' do | |
42 | expect(subject.map(&:hostname)).to eq %w{example1.com example3.com} | |
43 | end | |
44 | end | |
45 | end | |
46 | ||
47 | describe 'fetching servers by multiple roles' do | |
48 | it "does not confuse the last role with options" do | |
49 | expect(dsl.roles(:app, :web).count).to eq 4 | |
50 | expect(dsl.roles(:app, :web, filter: :active).count).to eq 2 | |
51 | end | |
52 | end | |
53 | ||
54 | describe 'fetching servers by role' do | |
55 | subject { dsl.roles(:app) } | |
56 | ||
57 | it 'returns the servers' do | |
58 | expect(subject.map(&:hostname)).to eq %w{example3.com example4.com} | |
59 | end | |
60 | end | |
61 | ||
62 | describe 'fetching servers by an array of roles' do | |
63 | subject { dsl.roles([:app]) } | |
64 | ||
65 | it 'returns the servers' do | |
66 | expect(subject.map(&:hostname)).to eq %w{example3.com example4.com} | |
67 | end | |
68 | end | |
69 | ||
70 | describe 'fetching filtered servers by role' do | |
71 | subject { dsl.roles(:app, filter: :active) } | |
72 | ||
73 | it 'returns the servers' do | |
74 | expect(subject.map(&:hostname)).to eq %w{example3.com} | |
75 | end | |
76 | end | |
77 | ||
78 | describe 'fetching selected servers by role' do | |
79 | subject { dsl.roles(:app, select: :active) } | |
80 | ||
81 | it 'returns the servers' do | |
82 | expect(subject.map(&:hostname)).to eq %w{example3.com} | |
83 | end | |
84 | end | |
85 | ||
86 | describe 'fetching the primary server by role' do | |
87 | context 'when inferring primary status based on order' do | |
88 | subject { dsl.primary(:web) } | |
89 | it 'returns the servers' do | |
90 | expect(subject.hostname).to eq 'example1.com' | |
91 | end | |
92 | end | |
93 | ||
94 | context 'when the attribute `primary` is explicity set' do | |
95 | subject { dsl.primary(:app) } | |
96 | it 'returns the servers' do | |
97 | expect(subject.hostname).to eq 'example4.com' | |
98 | end | |
99 | end | |
100 | end | |
101 | ||
102 | end | |
103 | ||
104 | describe 'when defining role with reserved name' do | |
105 | it 'fails with ArgumentError' do | |
106 | expect { | |
107 | dsl.role :all, %w{example1.com} | |
108 | }.to raise_error(ArgumentError, "all reserved name for role. Please choose another name") | |
109 | end | |
110 | end | |
111 | ||
112 | describe 'when defining hosts using the `role` syntax' do | |
113 | before do | |
114 | dsl.role :web, %w{example1.com example2.com example3.com} | |
115 | dsl.role :web, %w{example1.com}, active: true | |
116 | dsl.role :app, %w{example3.com example4.com} | |
117 | dsl.role :app, %w{example3.com}, active: true | |
118 | dsl.role :app, %w{example4.com}, primary: true | |
119 | dsl.role :db, %w{example5.com}, no_release: true | |
120 | end | |
121 | ||
122 | describe 'fetching all servers' do | |
123 | subject { dsl.roles(:all) } | |
124 | ||
125 | it 'returns all servers' do | |
126 | expect(subject.map(&:hostname)).to eq %w{example1.com example2.com example3.com example4.com example5.com} | |
127 | end | |
128 | end | |
129 | ||
130 | describe 'fetching all release servers' do | |
131 | ||
132 | context 'with no additional options' do | |
133 | subject { dsl.release_roles(:all) } | |
134 | ||
135 | it 'returns all release servers' do | |
136 | expect(subject.map(&:hostname)).to eq %w{example1.com example2.com example3.com example4.com} | |
137 | end | |
138 | end | |
139 | ||
140 | context 'with filter options' do | |
141 | subject { dsl.release_roles(:all, filter: :active) } | |
142 | ||
143 | it 'returns all release servers that match the filter' do | |
144 | expect(subject.map(&:hostname)).to eq %w{example1.com example3.com} | |
145 | end | |
146 | end | |
147 | end | |
148 | ||
149 | ||
150 | describe 'fetching servers by role' do | |
151 | subject { dsl.roles(:app) } | |
152 | ||
153 | it 'returns the servers' do | |
154 | expect(subject.map(&:hostname)).to eq %w{example3.com example4.com} | |
155 | end | |
156 | end | |
157 | ||
158 | describe 'fetching servers by an array of roles' do | |
159 | subject { dsl.roles([:app]) } | |
160 | ||
161 | it 'returns the servers' do | |
162 | expect(subject.map(&:hostname)).to eq %w{example3.com example4.com} | |
163 | end | |
164 | end | |
165 | ||
166 | describe 'fetching filtered servers by role' do | |
167 | subject { dsl.roles(:app, filter: :active) } | |
168 | ||
169 | it 'returns the servers' do | |
170 | expect(subject.map(&:hostname)).to eq %w{example3.com} | |
171 | end | |
172 | end | |
173 | ||
174 | describe 'fetching selected servers by role' do | |
175 | subject { dsl.roles(:app, select: :active) } | |
176 | ||
177 | it 'returns the servers' do | |
178 | expect(subject.map(&:hostname)).to eq %w{example3.com} | |
179 | end | |
180 | end | |
181 | ||
182 | describe 'fetching the primary server by role' do | |
183 | context 'when inferring primary status based on order' do | |
184 | subject { dsl.primary(:web) } | |
185 | it 'returns the servers' do | |
186 | expect(subject.hostname).to eq 'example1.com' | |
187 | end | |
188 | end | |
189 | ||
190 | context 'when the attribute `primary` is explicity set' do | |
191 | subject { dsl.primary(:app) } | |
192 | it 'returns the servers' do | |
193 | expect(subject.hostname).to eq 'example4.com' | |
194 | end | |
195 | end | |
196 | end | |
197 | ||
198 | end | |
199 | ||
200 | describe 'when defining a host using a combination of the `server` and `role` syntax' do | |
201 | ||
202 | before do | |
203 | dsl.server 'db@example1.com:1234', roles: %w{db}, active: true | |
204 | dsl.server 'root@example1.com:1234', roles: %w{web}, active: true | |
205 | dsl.server 'example1.com:5678', roles: %w{web}, active: true | |
206 | dsl.role :app, %w{deployer@example1.com:1234} | |
207 | dsl.role :app, %w{example1.com:5678} | |
208 | end | |
209 | ||
210 | describe 'fetching all servers' do | |
211 | subject { dsl.roles(:all).map { |server| "#{server.user}@#{server.hostname}:#{server.port}" } } | |
212 | ||
213 | it 'creates a server instance for each unique user@host:port combination' do | |
214 | expect(subject).to eq %w{db@example1.com:1234 root@example1.com:1234 @example1.com:5678 deployer@example1.com:1234} | |
215 | end | |
216 | end | |
217 | ||
218 | describe 'fetching servers for a role' do | |
219 | it 'roles defined using the `server` syntax are included' do | |
220 | expect(dsl.roles(:web)).to have(2).items | |
221 | end | |
222 | ||
223 | it 'roles defined using the `role` syntax are included' do | |
224 | expect(dsl.roles(:app)).to have(2).items | |
225 | end | |
226 | end | |
227 | ||
228 | end | |
229 | ||
230 | end | |
231 | ||
232 | describe 'setting and fetching variables' do | |
233 | ||
234 | before do | |
235 | dsl.set :scm, :git | |
236 | end | |
237 | ||
238 | context 'without a default' do | |
239 | context 'when the variables is defined' do | |
240 | it 'returns the variable' do | |
241 | expect(dsl.fetch(:scm)).to eq :git | |
242 | end | |
243 | end | |
244 | ||
245 | context 'when the variables is undefined' do | |
246 | it 'returns nil' do | |
247 | expect(dsl.fetch(:source_control)).to be_nil | |
248 | end | |
249 | end | |
250 | end | |
251 | ||
252 | context 'with a default' do | |
253 | context 'when the variables is defined' do | |
254 | it 'returns the variable' do | |
255 | expect(dsl.fetch(:scm, :svn)).to eq :git | |
256 | end | |
257 | end | |
258 | ||
259 | context 'when the variables is undefined' do | |
260 | it 'returns the default' do | |
261 | expect(dsl.fetch(:source_control, :svn)).to eq :svn | |
262 | end | |
263 | end | |
264 | end | |
265 | ||
266 | context 'with a block' do | |
267 | context 'when the variables is defined' do | |
268 | it 'returns the variable' do | |
269 | expect(dsl.fetch(:scm) { :svn }).to eq :git | |
270 | end | |
271 | end | |
272 | ||
273 | context 'when the variables is undefined' do | |
274 | it 'calls the block' do | |
275 | expect(dsl.fetch(:source_control) { :svn }).to eq :svn | |
276 | end | |
277 | end | |
278 | end | |
279 | ||
280 | end | |
281 | ||
282 | describe 'asking for a variable' do | |
283 | before do | |
284 | dsl.ask(:scm, :svn) | |
285 | $stdout.stubs(:puts) | |
286 | end | |
287 | ||
288 | context 'variable is provided' do | |
289 | before do | |
290 | $stdin.expects(:gets).returns('git') | |
291 | end | |
292 | ||
293 | it 'sets the input as the variable' do | |
294 | expect(dsl.fetch(:scm)).to eq 'git' | |
295 | end | |
296 | end | |
297 | ||
298 | context 'variable is not provided' do | |
299 | before do | |
300 | $stdin.expects(:gets).returns('') | |
301 | end | |
302 | ||
303 | it 'sets the variable as the default' do | |
304 | expect(dsl.fetch(:scm)).to eq :svn | |
305 | end | |
306 | end | |
307 | end | |
308 | ||
309 | describe 'checking for presence' do | |
310 | subject { dsl.any? :linked_files } | |
311 | ||
312 | before do | |
313 | dsl.set(:linked_files, linked_files) | |
314 | end | |
315 | ||
316 | context 'variable is an non-empty array' do | |
317 | let(:linked_files) { %w{1} } | |
318 | ||
319 | it { should be_true } | |
320 | end | |
321 | ||
322 | context 'variable is an empty array' do | |
323 | let(:linked_files) { [] } | |
324 | it { should be_false } | |
325 | end | |
326 | ||
327 | context 'variable exists, is not an array' do | |
328 | let(:linked_files) { stub } | |
329 | it { should be_true } | |
330 | end | |
331 | ||
332 | context 'variable is nil' do | |
333 | let(:linked_files) { nil } | |
334 | it { should be_false } | |
335 | end | |
336 | end | |
337 | ||
338 | describe 'configuration SSHKit' do | |
339 | let(:config) { SSHKit.config } | |
340 | let(:backend) { SSHKit.config.backend.config } | |
341 | let(:default_env) { { rails_env: :production } } | |
342 | ||
343 | before do | |
344 | dsl.set(:format, :dot) | |
345 | dsl.set(:log_level, :debug) | |
346 | dsl.set(:default_env, default_env) | |
347 | dsl.set(:pty, true) | |
348 | dsl.set(:connection_timeout, 10) | |
349 | dsl.set(:ssh_options, { | |
350 | keys: %w(/home/user/.ssh/id_rsa), | |
351 | forward_agent: false, | |
352 | auth_methods: %w(publickey password) | |
353 | }) | |
354 | dsl.configure_backend | |
355 | end | |
356 | ||
357 | it 'sets the output' do | |
358 | expect(config.output).to be_a SSHKit::Formatter::Dot | |
359 | end | |
360 | ||
361 | it 'sets the output verbosity' do | |
362 | expect(config.output_verbosity).to eq 0 | |
363 | end | |
364 | ||
365 | it 'sets the default env' do | |
366 | expect(config.default_env).to eq default_env | |
367 | end | |
368 | ||
369 | it 'sets the backend pty' do | |
370 | expect(backend.pty).to be_true | |
371 | end | |
372 | ||
373 | it 'sets the backend connection timeout' do | |
374 | expect(backend.connection_timeout).to eq 10 | |
375 | end | |
376 | ||
377 | it 'sets the backend ssh_options' do | |
378 | expect(backend.ssh_options[:keys]).to eq %w(/home/user/.ssh/id_rsa) | |
379 | expect(backend.ssh_options[:forward_agent]).to eq false | |
380 | expect(backend.ssh_options[:auth_methods]).to eq %w(publickey password) | |
381 | end | |
382 | ||
383 | end | |
384 | ||
385 | describe 'release path' do | |
386 | ||
387 | before do | |
388 | dsl.set(:deploy_to, '/var/www') | |
389 | end | |
390 | ||
391 | describe 'fetching release path' do | |
392 | subject { dsl.release_path } | |
393 | ||
394 | context 'where no release path has been set' do | |
395 | before do | |
396 | dsl.delete(:release_path) | |
397 | end | |
398 | ||
399 | it 'returns the `current_path` value' do | |
400 | expect(subject.to_s).to eq '/var/www/current' | |
401 | end | |
402 | end | |
403 | ||
404 | context 'where the release path has been set' do | |
405 | before do | |
406 | dsl.set(:release_path, '/var/www/release_path') | |
407 | end | |
408 | ||
409 | it 'returns the set `release_path` value' do | |
410 | expect(subject.to_s).to eq '/var/www/release_path' | |
411 | end | |
412 | end | |
413 | end | |
414 | ||
415 | describe 'setting release path' do | |
416 | let(:now) { Time.parse("Oct 21 16:29:00 2015") } | |
417 | subject { dsl.release_path } | |
418 | ||
419 | context 'without a timestamp' do | |
420 | before do | |
421 | dsl.env.expects(:timestamp).returns(now) | |
422 | dsl.set_release_path | |
423 | end | |
424 | ||
425 | it 'returns the release path with the current env timestamp' do | |
426 | expect(subject.to_s).to eq '/var/www/releases/20151021162900' | |
427 | end | |
428 | end | |
429 | ||
430 | context 'with a timestamp' do | |
431 | before do | |
432 | dsl.set_release_path('timestamp') | |
433 | end | |
434 | ||
435 | it 'returns the release path with the timestamp' do | |
436 | expect(subject.to_s).to eq '/var/www/releases/timestamp' | |
437 | end | |
438 | end | |
439 | end | |
440 | ||
441 | describe 'setting deploy configuration path' do | |
442 | subject { dsl.deploy_config_path.to_s } | |
443 | ||
444 | context 'where no config path is set' do | |
445 | before do | |
446 | dsl.delete(:deploy_config_path) | |
447 | end | |
448 | ||
449 | it 'returns "config/deploy.rb"' do | |
450 | expect(subject).to eq 'config/deploy.rb' | |
451 | end | |
452 | end | |
453 | ||
454 | context 'where a custom path is set' do | |
455 | before do | |
456 | dsl.set(:deploy_config_path, 'my/custom/path.rb') | |
457 | end | |
458 | ||
459 | it 'returns the custom path' do | |
460 | expect(subject).to eq 'my/custom/path.rb' | |
461 | end | |
462 | end | |
463 | end | |
464 | ||
465 | describe 'setting stage configuration path' do | |
466 | subject { dsl.stage_config_path.to_s } | |
467 | ||
468 | context 'where no config path is set' do | |
469 | ||
470 | before do | |
471 | dsl.delete(:stage_config_path) | |
472 | end | |
473 | ||
474 | it 'returns "config/deploy"' do | |
475 | expect(subject).to eq 'config/deploy' | |
476 | end | |
477 | end | |
478 | ||
479 | context 'where a custom path is set' do | |
480 | before do | |
481 | dsl.set(:stage_config_path, 'my/custom/path') | |
482 | end | |
483 | ||
484 | it 'returns the custom path' do | |
485 | expect(subject).to eq 'my/custom/path' | |
486 | end | |
487 | end | |
488 | end | |
489 | end | |
490 | end |
0 | require 'spec_helper' | |
1 | require 'support/test_app' | |
2 | require 'support/matchers' | |
3 | ||
4 | include TestApp | |
5 | ||
6 |
0 | require 'spec_helper' | |
1 | ||
2 | describe Capistrano::Application do | |
3 | ||
4 | it "provides a --trace option which enables SSHKit/NetSSH trace output" | |
5 | ||
6 | it "provides a --format option which enables the choice of output formatting" | |
7 | ||
8 | it "identifies itself as cap and not rake" do | |
9 | out, _ = capture_io do | |
10 | flags '--help', '-h' | |
11 | end | |
12 | out.lines.first.should match(/cap \[-f rakefile\]/) | |
13 | end | |
14 | ||
15 | it "overrides the rake method, but still prints the rake version" do | |
16 | out, _ = capture_io do | |
17 | flags '--version', '-V' | |
18 | end | |
19 | out.should match(/\bCapistrano Version\b/) | |
20 | out.should match(/\b#{Capistrano::VERSION}\b/) | |
21 | out.should match(/\bRake Version\b/) | |
22 | out.should match(/\b#{RAKEVERSION}\b/) | |
23 | end | |
24 | ||
25 | def flags(*sets) | |
26 | sets.each do |set| | |
27 | ARGV.clear | |
28 | @exit = catch(:system_exit) { command_line(*set) } | |
29 | end | |
30 | yield(subject.options) if block_given? | |
31 | end | |
32 | ||
33 | def command_line(*options) | |
34 | options.each { |opt| ARGV << opt } | |
35 | def subject.exit(*args) | |
36 | throw(:system_exit, :exit) | |
37 | end | |
38 | subject.run | |
39 | subject.options | |
40 | end | |
41 | ||
42 | def capture_io | |
43 | require 'stringio' | |
44 | ||
45 | orig_stdout, orig_stderr = $stdout, $stderr | |
46 | captured_stdout, captured_stderr = StringIO.new, StringIO.new | |
47 | $stdout, $stderr = captured_stdout, captured_stderr | |
48 | ||
49 | yield | |
50 | ||
51 | return captured_stdout.string, captured_stderr.string | |
52 | ensure | |
53 | $stdout = orig_stdout | |
54 | $stderr = orig_stderr | |
55 | end | |
56 | ||
57 | end |
0 | require 'spec_helper' | |
1 | ||
2 | module Capistrano | |
3 | class Configuration | |
4 | ||
5 | describe Question do | |
6 | ||
7 | let(:question) { Question.new(env, key, default) } | |
8 | let(:default) { :default } | |
9 | let(:key) { :branch } | |
10 | let(:env) { stub } | |
11 | ||
12 | describe '.new' do | |
13 | it 'takes a key, default' do | |
14 | question | |
15 | end | |
16 | end | |
17 | ||
18 | describe '#call' do | |
19 | subject { question.call } | |
20 | ||
21 | context 'value is entered' do | |
22 | let(:branch) { 'branch' } | |
23 | ||
24 | before do | |
25 | $stdout.expects(:puts).with('Please enter branch: |default|') | |
26 | $stdin.expects(:gets).returns(branch) | |
27 | end | |
28 | ||
29 | it 'sets the value' do | |
30 | env.expects(:set).with(key, branch) | |
31 | question.call | |
32 | end | |
33 | end | |
34 | ||
35 | context 'value is not entered' do | |
36 | let(:branch) { default } | |
37 | ||
38 | before do | |
39 | $stdout.expects(:puts).with('Please enter branch: |default|') | |
40 | $stdin.expects(:gets).returns('') | |
41 | end | |
42 | ||
43 | it 'sets the default as the value' do | |
44 | env.expects(:set).with(key, branch) | |
45 | question.call | |
46 | end | |
47 | ||
48 | end | |
49 | end | |
50 | end | |
51 | ||
52 | end | |
53 | end |
0 | require 'spec_helper' | |
1 | ||
2 | module Capistrano | |
3 | class Configuration | |
4 | describe Server do | |
5 | let(:server) { Server.new('root@hostname:1234') } | |
6 | ||
7 | describe 'adding a role' do | |
8 | subject { server.add_role(:test) } | |
9 | it 'adds the role' do | |
10 | expect{subject}.to change{server.roles.size}.from(0).to(1) | |
11 | end | |
12 | end | |
13 | ||
14 | describe 'adding roles' do | |
15 | subject { server.add_roles([:things, :stuff]) } | |
16 | it 'adds the roles' do | |
17 | expect{subject}.to change{server.roles.size}.from(0).to(2) | |
18 | end | |
19 | end | |
20 | ||
21 | ||
22 | describe 'checking roles' do | |
23 | subject { server.has_role?(:test) } | |
24 | ||
25 | before do | |
26 | server.add_role(:test) | |
27 | end | |
28 | ||
29 | it 'adds the role' do | |
30 | expect{subject}.to be_true | |
31 | end | |
32 | end | |
33 | ||
34 | describe 'comparing identity' do | |
35 | subject { server.matches? Server[hostname] } | |
36 | ||
37 | context 'with the same user, hostname and port' do | |
38 | let(:hostname) { 'root@hostname:1234' } | |
39 | it { should be_true } | |
40 | end | |
41 | ||
42 | context 'with a different user' do | |
43 | let(:hostname) { 'deployer@hostname:1234' } | |
44 | it { should be_false } | |
45 | end | |
46 | ||
47 | context 'with a different port' do | |
48 | let(:hostname) { 'root@hostname:5678' } | |
49 | it { should be_false } | |
50 | end | |
51 | ||
52 | context 'with a different hostname' do | |
53 | let(:hostname) { 'root@otherserver:1234' } | |
54 | it { should be_false } | |
55 | end | |
56 | end | |
57 | ||
58 | describe 'identifying as primary' do | |
59 | subject { server.primary } | |
60 | context 'server is primary' do | |
61 | before do | |
62 | server.set(:primary, true) | |
63 | end | |
64 | it 'returns self' do | |
65 | expect(subject).to eq server | |
66 | end | |
67 | end | |
68 | ||
69 | context 'server is not primary' do | |
70 | it 'is falesy' do | |
71 | expect(subject).to be_false | |
72 | end | |
73 | end | |
74 | end | |
75 | ||
76 | describe 'assigning properties' do | |
77 | ||
78 | before do | |
79 | server.with(properties) | |
80 | end | |
81 | ||
82 | context 'properties contains roles' do | |
83 | let(:properties) { {roles: [:clouds]} } | |
84 | ||
85 | it 'adds the roles' do | |
86 | expect(server.roles.first).to eq :clouds | |
87 | end | |
88 | end | |
89 | ||
90 | context 'properties contains user' do | |
91 | let(:properties) { {user: 'tomc'} } | |
92 | ||
93 | it 'sets the user' do | |
94 | expect(server.user).to eq 'tomc' | |
95 | end | |
96 | end | |
97 | ||
98 | context 'properties contains port' do | |
99 | let(:properties) { {port: 2222} } | |
100 | ||
101 | it 'sets the port' do | |
102 | expect(server.port).to eq 2222 | |
103 | end | |
104 | end | |
105 | ||
106 | context 'properties contains key' do | |
107 | let(:properties) { {key: '/key'} } | |
108 | ||
109 | it 'adds the key' do | |
110 | expect(server.keys).to include '/key' | |
111 | end | |
112 | end | |
113 | ||
114 | context 'properties contains password' do | |
115 | let(:properties) { {password: 'supersecret'} } | |
116 | ||
117 | it 'adds the key' do | |
118 | expect(server.password).to eq 'supersecret' | |
119 | end | |
120 | end | |
121 | ||
122 | context 'new properties' do | |
123 | let(:properties) { { webscales: 5 } } | |
124 | ||
125 | it 'adds the properties' do | |
126 | expect(server.properties.webscales).to eq 5 | |
127 | end | |
128 | end | |
129 | ||
130 | context 'existing properties' do | |
131 | let(:properties) { { webscales: 6 } } | |
132 | ||
133 | it 'keeps the existing properties' do | |
134 | expect(server.properties.webscales).to eq 6 | |
135 | server.properties.webscales = 5 | |
136 | expect(server.properties.webscales).to eq 5 | |
137 | end | |
138 | end | |
139 | end | |
140 | ||
141 | describe '#include?' do | |
142 | let(:options) { {} } | |
143 | ||
144 | subject { server.select?(options) } | |
145 | ||
146 | before do | |
147 | server.properties.active = true | |
148 | end | |
149 | ||
150 | context 'options are empty' do | |
151 | it { should be_true } | |
152 | end | |
153 | ||
154 | context 'value is a symbol' do | |
155 | context 'value matches server property' do | |
156 | ||
157 | context 'with :filter' do | |
158 | let(:options) { { filter: :active }} | |
159 | it { should be_true } | |
160 | end | |
161 | ||
162 | context 'with :select' do | |
163 | let(:options) { { select: :active }} | |
164 | it { should be_true } | |
165 | end | |
166 | ||
167 | context 'with :exclude' do | |
168 | let(:options) { { exclude: :active }} | |
169 | it { should be_false } | |
170 | end | |
171 | end | |
172 | ||
173 | context 'value does not match server properly' do | |
174 | context 'with :filter' do | |
175 | let(:options) { { filter: :inactive }} | |
176 | it { should be_false } | |
177 | end | |
178 | ||
179 | context 'with :select' do | |
180 | let(:options) { { select: :inactive }} | |
181 | it { should be_false } | |
182 | end | |
183 | ||
184 | context 'with :exclude' do | |
185 | let(:options) { { exclude: :inactive }} | |
186 | it { should be_true } | |
187 | end | |
188 | end | |
189 | end | |
190 | ||
191 | context 'value is a proc' do | |
192 | context 'value matches server property' do | |
193 | ||
194 | context 'with :filter' do | |
195 | let(:options) { { filter: ->(s) { s.properties.active } } } | |
196 | it { should be_true } | |
197 | end | |
198 | ||
199 | context 'with :select' do | |
200 | let(:options) { { select: ->(s) { s.properties.active } } } | |
201 | it { should be_true } | |
202 | end | |
203 | ||
204 | context 'with :exclude' do | |
205 | let(:options) { { exclude: ->(s) { s.properties.active } } } | |
206 | it { should be_false } | |
207 | end | |
208 | ||
209 | end | |
210 | ||
211 | context 'value does not match server properly' do | |
212 | context 'with :filter' do | |
213 | let(:options) { { filter: ->(s) { s.properties.inactive } } } | |
214 | it { should be_false } | |
215 | end | |
216 | ||
217 | context 'with :select' do | |
218 | let(:options) { { select: ->(s) { s.properties.inactive } } } | |
219 | it { should be_false } | |
220 | end | |
221 | ||
222 | context 'with :exclude' do | |
223 | let(:options) { { exclude: ->(s) { s.properties.inactive } } } | |
224 | it { should be_true } | |
225 | end | |
226 | ||
227 | end | |
228 | end | |
229 | ||
230 | end | |
231 | ||
232 | describe 'assign ssh_options' do | |
233 | let(:server) { Server.new('user_name@hostname') } | |
234 | ||
235 | context 'defaults' do | |
236 | it 'forward agent' do | |
237 | expect(server.netssh_options[:forward_agent]).to eq true | |
238 | end | |
239 | it 'contains user' do | |
240 | expect(server.netssh_options[:user]).to eq 'user_name' | |
241 | end | |
242 | end | |
243 | ||
244 | context 'custom' do | |
245 | let(:properties) do | |
246 | { ssh_options: { | |
247 | user: 'another_user', | |
248 | keys: %w(/home/another_user/.ssh/id_rsa), | |
249 | forward_agent: false, | |
250 | auth_methods: %w(publickey password) } } | |
251 | end | |
252 | ||
253 | before do | |
254 | server.with(properties) | |
255 | end | |
256 | ||
257 | it 'not forward agent' do | |
258 | expect(server.netssh_options[:forward_agent]).to eq false | |
259 | end | |
260 | it 'contains correct user' do | |
261 | expect(server.netssh_options[:user]).to eq 'another_user' | |
262 | end | |
263 | it 'contains keys' do | |
264 | expect(server.netssh_options[:keys]).to eq %w(/home/another_user/.ssh/id_rsa) | |
265 | end | |
266 | it 'contains auth_methods' do | |
267 | expect(server.netssh_options[:auth_methods]).to eq %w(publickey password) | |
268 | end | |
269 | end | |
270 | ||
271 | end | |
272 | ||
273 | describe ".[]" do | |
274 | it 'creates a server if its argument is not already a server' do | |
275 | expect(Server['hostname:1234']).to be_a Server | |
276 | end | |
277 | ||
278 | it 'returns its argument if it is already a server' do | |
279 | expect(Server[server]).to be server | |
280 | end | |
281 | end | |
282 | end | |
283 | end | |
284 | end |
0 | require 'spec_helper' | |
1 | ||
2 | module Capistrano | |
3 | class Configuration | |
4 | class Servers | |
5 | ||
6 | describe HostFilter do | |
7 | let(:host_filter) { HostFilter.new(available) } | |
8 | let(:available) { [ Server.new('server1'), Server.new('server2'), Server.new('server3') ] } | |
9 | ||
10 | describe '#new' do | |
11 | it 'takes one array of hostnames' do | |
12 | expect(host_filter) | |
13 | end | |
14 | end | |
15 | ||
16 | describe '.for' do | |
17 | ||
18 | subject { HostFilter.for(available) } | |
19 | ||
20 | context 'without env vars' do | |
21 | it 'returns all available hosts' do | |
22 | expect(subject).to eq available | |
23 | end | |
24 | end | |
25 | ||
26 | context 'with ENV vars' do | |
27 | before do | |
28 | ENV.stubs(:[]).with('HOSTS').returns('server1,server2') | |
29 | end | |
30 | ||
31 | it 'returns all required hosts defined in HOSTS' do | |
32 | expect(subject).to eq [Server.new('server1'), Server.new('server2')] | |
33 | end | |
34 | end | |
35 | ||
36 | context 'with configuration filters' do | |
37 | before do | |
38 | Configuration.env.set(:filter, hosts: %w{server1 server2}) | |
39 | end | |
40 | ||
41 | it 'returns all required hosts defined in the filter' do | |
42 | expect(subject).to eq [Server.new('server1'), Server.new('server2')] | |
43 | end | |
44 | ||
45 | after do | |
46 | Configuration.env.delete(:filter) | |
47 | end | |
48 | end | |
49 | ||
50 | context 'with a single configuration filter' do | |
51 | before do | |
52 | Configuration.env.set(:filter, hosts: 'server3') | |
53 | end | |
54 | ||
55 | it 'returns all required hosts defined in the filter' do | |
56 | expect(subject).to eq [Server.new('server3')] | |
57 | end | |
58 | ||
59 | after do | |
60 | Configuration.env.delete(:filter) | |
61 | end | |
62 | end | |
63 | ||
64 | context 'with configuration filters and ENV vars' do | |
65 | before do | |
66 | Configuration.env.set(:filter, hosts: 'server1') | |
67 | ENV.stubs(:[]).with('HOSTS').returns('server3') | |
68 | end | |
69 | ||
70 | it 'returns all required hosts defined in the filter' do | |
71 | expect(subject).to eq [Server.new('server1'), Server.new('server3')] | |
72 | end | |
73 | ||
74 | after do | |
75 | Configuration.env.delete(:filter) | |
76 | end | |
77 | end | |
78 | end | |
79 | end | |
80 | ||
81 | end | |
82 | end | |
83 | end |
0 | require 'spec_helper' | |
1 | ||
2 | module Capistrano | |
3 | class Configuration | |
4 | class Servers | |
5 | ||
6 | describe RoleFilter do | |
7 | let(:role_filter) { RoleFilter.new(required, available) } | |
8 | let(:required) { [] } | |
9 | let(:available) { [:web, :app, :db] } | |
10 | ||
11 | describe '#new' do | |
12 | it 'takes two arrays of role names' do | |
13 | expect(role_filter) | |
14 | end | |
15 | end | |
16 | ||
17 | describe '.for' do | |
18 | ||
19 | subject { RoleFilter.for(required, available) } | |
20 | ||
21 | context 'without env vars' do | |
22 | context ':all required' do | |
23 | let(:required) { [:all] } | |
24 | ||
25 | it 'returns all available names' do | |
26 | expect(subject).to eq available | |
27 | end | |
28 | end | |
29 | ||
30 | context 'role names required' do | |
31 | let(:required) { [:web, :app] } | |
32 | it 'returns all required names' do | |
33 | expect(subject).to eq required | |
34 | end | |
35 | end | |
36 | end | |
37 | ||
38 | context 'with ENV vars' do | |
39 | before do | |
40 | ENV.stubs(:[]).with('ROLES').returns('app,web') | |
41 | end | |
42 | ||
43 | context ':all required' do | |
44 | let(:required) { [:all] } | |
45 | ||
46 | it 'returns available names defined in ROLES' do | |
47 | expect(subject).to eq [:app, :web] | |
48 | end | |
49 | end | |
50 | ||
51 | context 'role names required' do | |
52 | let(:required) { [:web, :db] } | |
53 | it 'returns all required names defined in ROLES' do | |
54 | expect(subject).to eq [:web] | |
55 | end | |
56 | end | |
57 | end | |
58 | ||
59 | context 'with configuration filters' do | |
60 | before do | |
61 | Configuration.env.set(:filter, roles: %w{app web}) | |
62 | end | |
63 | ||
64 | context ':all required' do | |
65 | let(:required) { [:all] } | |
66 | ||
67 | it 'returns available names defined in the filter' do | |
68 | expect(subject).to eq [:app, :web] | |
69 | end | |
70 | end | |
71 | ||
72 | context 'role names required' do | |
73 | let(:required) { [:web, :db] } | |
74 | it 'returns all required names defined in the filter' do | |
75 | expect(subject).to eq [:web] | |
76 | end | |
77 | end | |
78 | ||
79 | after do | |
80 | Configuration.env.delete(:filter) | |
81 | end | |
82 | end | |
83 | ||
84 | context 'with a single configuration filter' do | |
85 | before do | |
86 | Configuration.env.set(:filter, roles: 'web') | |
87 | end | |
88 | ||
89 | context ':all required' do | |
90 | let(:required) { [:all] } | |
91 | ||
92 | it 'returns available names defined in the filter' do | |
93 | expect(subject).to eq [:web] | |
94 | end | |
95 | end | |
96 | ||
97 | context 'role names required' do | |
98 | let(:required) { [:web, :db] } | |
99 | it 'returns all required names defined in the filter' do | |
100 | expect(subject).to eq [:web] | |
101 | end | |
102 | end | |
103 | ||
104 | after do | |
105 | Configuration.env.delete(:filter) | |
106 | end | |
107 | end | |
108 | ||
109 | context 'with configuration filters and ENV vars' do | |
110 | before do | |
111 | Configuration.env.set(:filter, roles: %w{app}) | |
112 | ENV.stubs(:[]).with('ROLES').returns('web') | |
113 | end | |
114 | ||
115 | context ':all required' do | |
116 | let(:required) { [:all] } | |
117 | ||
118 | it 'returns available names defined in the filter' do | |
119 | expect(subject).to eq [:web, :app] | |
120 | end | |
121 | end | |
122 | ||
123 | context 'role names required' do | |
124 | let(:required) { [:web, :db] } | |
125 | it 'returns all required names defined in the filter' do | |
126 | expect(subject).to eq [:web] | |
127 | end | |
128 | end | |
129 | ||
130 | after do | |
131 | Configuration.env.delete(:filter) | |
132 | end | |
133 | end | |
134 | end | |
135 | end | |
136 | ||
137 | end | |
138 | end | |
139 | end |
0 | require 'spec_helper' | |
1 | ||
2 | module Capistrano | |
3 | class Configuration | |
4 | describe Servers do | |
5 | let(:servers) { Servers.new } | |
6 | ||
7 | describe 'adding a role' do | |
8 | ||
9 | it 'adds two new server instances' do | |
10 | expect{servers.add_role(:app, %w{1 2})}. | |
11 | to change{servers.count}.from(0).to(2) | |
12 | end | |
13 | ||
14 | it 'handles de-duplification within roles' do | |
15 | servers.add_role(:app, %w{1}) | |
16 | servers.add_role(:app, %w{1}) | |
17 | expect(servers.count).to eq 1 | |
18 | end | |
19 | ||
20 | it 'accepts instances of server objects' do | |
21 | servers.add_role(:app, [Capistrano::Configuration::Server.new('example.net'), 'example.com']) | |
22 | expect(servers.roles_for([:app]).length).to eq 2 | |
23 | end | |
24 | ||
25 | it 'accepts non-enumerable types' do | |
26 | servers.add_role(:app, '1') | |
27 | expect(servers.roles_for([:app]).count).to eq 1 | |
28 | end | |
29 | ||
30 | end | |
31 | ||
32 | describe 'adding a role to an existing server' do | |
33 | before do | |
34 | servers.add_role(:web, %w{1 2}) | |
35 | servers.add_role(:app, %w{1 2}) | |
36 | end | |
37 | ||
38 | it 'adds new roles to existing servers' do | |
39 | expect(servers.count).to eq 2 | |
40 | end | |
41 | ||
42 | end | |
43 | ||
44 | describe 'collecting server roles' do | |
45 | let(:app) { Set.new([:app]) } | |
46 | let(:web_app) { Set.new([:web, :app]) } | |
47 | let(:web) { Set.new([:web]) } | |
48 | ||
49 | before do | |
50 | servers.add_role(:app, %w{1 2 3}) | |
51 | servers.add_role(:web, %w{2 3 4}) | |
52 | end | |
53 | ||
54 | it 'returns an array of the roles' do | |
55 | expect(servers.roles_for([:app]).collect(&:roles)).to eq [app, web_app, web_app] | |
56 | expect(servers.roles_for([:web]).collect(&:roles)).to eq [web_app, web_app, web] | |
57 | end | |
58 | end | |
59 | ||
60 | describe 'finding the primary server' do | |
61 | it 'takes the first server if none have the primary property' do | |
62 | servers.add_role(:app, %w{1 2}) | |
63 | servers.fetch_primary(:app).hostname.should == '1' | |
64 | end | |
65 | ||
66 | it 'takes the first server with the primary have the primary flag' do | |
67 | servers.add_role(:app, %w{1 2}) | |
68 | servers.add_host('2', primary: true) | |
69 | servers.fetch_primary(:app).hostname.should == '2' | |
70 | end | |
71 | end | |
72 | ||
73 | describe 'fetching servers' do | |
74 | before do | |
75 | servers.add_role(:app, %w{1 2}) | |
76 | servers.add_role(:web, %w{2 3}) | |
77 | end | |
78 | ||
79 | it 'returns the correct app servers' do | |
80 | expect(servers.roles_for([:app]).map(&:hostname)).to eq %w{1 2} | |
81 | end | |
82 | ||
83 | it 'returns the correct web servers' do | |
84 | expect(servers.roles_for([:web]).map(&:hostname)).to eq %w{2 3} | |
85 | end | |
86 | ||
87 | it 'returns the correct app and web servers' do | |
88 | expect(servers.roles_for([:app, :web]).map(&:hostname)).to eq %w{1 2 3} | |
89 | end | |
90 | ||
91 | it 'returns all servers' do | |
92 | expect(servers.roles_for([:all]).map(&:hostname)).to eq %w{1 2 3} | |
93 | end | |
94 | end | |
95 | ||
96 | describe 'adding a server' do | |
97 | ||
98 | before do | |
99 | servers.add_host('1', roles: [:app, 'web'], test: :value) | |
100 | end | |
101 | ||
102 | it 'can create a server with properties' do | |
103 | expect(servers.roles_for([:app]).first.hostname).to eq '1' | |
104 | expect(servers.roles_for([:web]).first.hostname).to eq '1' | |
105 | expect(servers.roles_for([:all]).first.properties.test).to eq :value | |
106 | expect(servers.roles_for([:all]).first.properties.keys).to eq [:test] | |
107 | end | |
108 | ||
109 | it 'can accept multiple servers with the same hostname but different ports or users' do | |
110 | servers.add_host('1', roles: [:app, 'web'], test: :value, port: 12) | |
111 | servers.add_host('1', roles: [:app, 'web'], test: :value, port: 34) | |
112 | servers.add_host('1', roles: [:app, 'web'], test: :value, user: 'root') | |
113 | servers.add_host('1', roles: [:app, 'web'], test: :value, user: 'deployer') | |
114 | servers.add_host('1', roles: [:app, 'web'], test: :value, user: 'root', port: 34) | |
115 | servers.add_host('1', roles: [:app, 'web'], test: :value, user: 'deployer', port: 34) | |
116 | servers.add_host('1', roles: [:app, 'web'], test: :value, user: 'deployer', port: 56) | |
117 | servers.should have(8).items | |
118 | end | |
119 | end | |
120 | ||
121 | describe 'selecting roles' do | |
122 | ||
123 | before do | |
124 | servers.add_host('1', roles: :app, active: true) | |
125 | servers.add_host('2', roles: :app) | |
126 | end | |
127 | ||
128 | it 'is empty if the filter would remove all matching hosts' do | |
129 | expect(servers.roles_for([:app, select: :inactive])).to be_empty | |
130 | end | |
131 | ||
132 | it 'can filter hosts by properties on the host object using symbol as shorthand' do | |
133 | expect(servers.roles_for([:app, filter: :active]).length).to eq 1 | |
134 | end | |
135 | ||
136 | it 'can select hosts by properties on the host object using symbol as shorthand' do | |
137 | expect(servers.roles_for([:app, select: :active]).length).to eq 1 | |
138 | end | |
139 | ||
140 | it 'can filter hosts by properties on the host using a regular proc' do | |
141 | expect(servers.roles_for([:app, filter: ->(h) { h.properties.active }]).length).to eq 1 | |
142 | end | |
143 | ||
144 | it 'can select hosts by properties on the host using a regular proc' do | |
145 | expect(servers.roles_for([:app, select: ->(h) { h.properties.active }]).length).to eq 1 | |
146 | end | |
147 | ||
148 | it 'is empty if the regular proc filter would remove all matching hosts' do | |
149 | expect(servers.roles_for([:app, select: ->(h) { h.properties.inactive }])).to be_empty | |
150 | end | |
151 | ||
152 | end | |
153 | ||
154 | describe 'excluding by property' do | |
155 | ||
156 | before do | |
157 | servers.add_host('1', roles: :app, active: true) | |
158 | servers.add_host('2', roles: :app, active: true, no_release: true) | |
159 | end | |
160 | ||
161 | it 'is empty if the filter would remove all matching hosts' do | |
162 | hosts = servers.roles_for([:app, exclude: :active]) | |
163 | expect(hosts.map(&:hostname)).to be_empty | |
164 | end | |
165 | ||
166 | it 'returns the servers without the attributes specified' do | |
167 | hosts = servers.roles_for([:app, exclude: :no_release]) | |
168 | expect(hosts.map(&:hostname)).to eq %w{1} | |
169 | end | |
170 | ||
171 | it 'can exclude hosts by properties on the host using a regular proc' do | |
172 | hosts = servers.roles_for([:app, exclude: ->(h) { h.properties.no_release }]) | |
173 | expect(hosts.map(&:hostname)).to eq %w{1} | |
174 | end | |
175 | ||
176 | it 'is empty if the regular proc filter would remove all matching hosts' do | |
177 | hosts = servers.roles_for([:app, exclude: ->(h) { h.properties.active }]) | |
178 | expect(hosts.map(&:hostname)).to be_empty | |
179 | end | |
180 | ||
181 | end | |
182 | ||
183 | describe 'filtering roles' do | |
184 | ||
185 | before do | |
186 | ENV.stubs(:[]).with('ROLES').returns('web,db') | |
187 | ENV.stubs(:[]).with('HOSTS').returns(nil) | |
188 | servers.add_host('1', roles: :app, active: true) | |
189 | servers.add_host('2', roles: :app) | |
190 | servers.add_host('3', roles: :web) | |
191 | servers.add_host('4', roles: :web) | |
192 | servers.add_host('5', roles: :db) | |
193 | end | |
194 | ||
195 | subject { servers.roles_for(roles).map(&:hostname) } | |
196 | ||
197 | context 'when selecting all roles' do | |
198 | let(:roles) { [:all] } | |
199 | ||
200 | it 'returns the roles specified by ROLE' do | |
201 | expect(subject).to eq %w{3 4 5} | |
202 | end | |
203 | end | |
204 | ||
205 | context 'when selecting roles included in ROLE' do | |
206 | let(:roles) { [:app, :web] } | |
207 | ||
208 | it 'returns only roles that match ROLE' do | |
209 | expect(subject).to eq %w{3 4} | |
210 | end | |
211 | end | |
212 | ||
213 | context 'when selecting roles not included in ROLE' do | |
214 | let(:roles) { [:app] } | |
215 | ||
216 | it 'is empty' do | |
217 | expect(subject).to be_empty | |
218 | end | |
219 | end | |
220 | end | |
221 | ||
222 | end | |
223 | end | |
224 | end |
0 | require 'spec_helper' | |
1 | ||
2 | module Capistrano | |
3 | describe Configuration do | |
4 | let(:config) { Configuration.new } | |
5 | let(:servers) { stub } | |
6 | ||
7 | describe '.env' do | |
8 | it 'is a global accessor to a single instance' do | |
9 | Configuration.env.set(:test, true) | |
10 | expect(Configuration.env.fetch(:test)).to be_true | |
11 | end | |
12 | end | |
13 | ||
14 | describe '.reset!' do | |
15 | it 'blows away the existing `env` and creates a new one' do | |
16 | old_env = Configuration.env | |
17 | Configuration.reset! | |
18 | expect(Configuration.env).not_to be old_env | |
19 | end | |
20 | end | |
21 | ||
22 | describe 'roles' do | |
23 | context 'adding a role' do | |
24 | subject { config.role(:app, %w{server1 server2}) } | |
25 | ||
26 | before do | |
27 | Configuration::Servers.expects(:new).returns(servers) | |
28 | servers.expects(:add_role).with(:app, %w{server1 server2}, {}) | |
29 | end | |
30 | ||
31 | it 'adds the role' do | |
32 | expect(subject) | |
33 | end | |
34 | end | |
35 | end | |
36 | ||
37 | describe 'setting and fetching' do | |
38 | subject { config.fetch(:key, :default) } | |
39 | ||
40 | context 'value is set' do | |
41 | before do | |
42 | config.set(:key, :value) | |
43 | end | |
44 | ||
45 | it 'returns the set value' do | |
46 | expect(subject).to eq :value | |
47 | end | |
48 | end | |
49 | ||
50 | context 'value is not set' do | |
51 | it 'returns the default value' do | |
52 | expect(subject).to eq :default | |
53 | end | |
54 | end | |
55 | ||
56 | context 'value is a proc' do | |
57 | subject { config.fetch(:key, Proc.new { :proc } ) } | |
58 | it 'calls the proc' do | |
59 | expect(subject).to eq :proc | |
60 | end | |
61 | end | |
62 | ||
63 | context 'value is a lambda' do | |
64 | subject { config.fetch(:key, lambda { :lambda } ) } | |
65 | it 'calls the lambda' do | |
66 | expect(subject).to eq :lambda | |
67 | end | |
68 | end | |
69 | ||
70 | context 'value inside proc inside a proc' do | |
71 | subject { config.fetch(:key, Proc.new { Proc.new { "some value" } } ) } | |
72 | it 'calls all procs and lambdas' do | |
73 | expect(subject).to eq "some value" | |
74 | end | |
75 | end | |
76 | ||
77 | context 'value inside lambda inside a lambda' do | |
78 | subject { config.fetch(:key, lambda { lambda { "some value" } } ) } | |
79 | it 'calls all procs and lambdas' do | |
80 | expect(subject).to eq "some value" | |
81 | end | |
82 | end | |
83 | ||
84 | context 'value inside lambda inside a proc' do | |
85 | subject { config.fetch(:key, Proc.new { lambda { "some value" } } ) } | |
86 | it 'calls all procs and lambdas' do | |
87 | expect(subject).to eq "some value" | |
88 | end | |
89 | end | |
90 | ||
91 | context 'value inside proc inside a lambda' do | |
92 | subject { config.fetch(:key, lambda { Proc.new { "some value" } } ) } | |
93 | it 'calls all procs and lambdas' do | |
94 | expect(subject).to eq "some value" | |
95 | end | |
96 | end | |
97 | ||
98 | context 'lambda with parameters' do | |
99 | subject { config.fetch(:key, lambda { |c| c }).call(42) } | |
100 | it 'is returned as a lambda' do | |
101 | expect(subject).to eq 42 | |
102 | end | |
103 | end | |
104 | ||
105 | context 'block is passed to fetch' do | |
106 | subject { config.fetch(:key, :default) { fail 'we need this!' } } | |
107 | ||
108 | it 'returns the block value' do | |
109 | expect { subject }.to raise_error | |
110 | end | |
111 | end | |
112 | end | |
113 | ||
114 | describe 'deleting' do | |
115 | before do | |
116 | config.set(:key, :value) | |
117 | end | |
118 | ||
119 | it 'deletes the value' do | |
120 | config.delete(:key) | |
121 | expect(config.fetch(:key)).to be_nil | |
122 | end | |
123 | end | |
124 | ||
125 | describe 'asking' do | |
126 | let(:question) { stub } | |
127 | ||
128 | before do | |
129 | Configuration::Question.expects(:new).with(config, :branch, :default). | |
130 | returns(question) | |
131 | end | |
132 | ||
133 | it 'prompts for the value when fetching' do | |
134 | config.ask(:branch, :default) | |
135 | expect(config.fetch(:branch)).to eq question | |
136 | end | |
137 | end | |
138 | ||
139 | describe 'setting the backend' do | |
140 | it 'by default, is SSHKit' do | |
141 | expect(config.backend).to eq SSHKit | |
142 | end | |
143 | ||
144 | it 'can be set to another class' do | |
145 | config.backend = :test | |
146 | expect(config.backend).to eq :test | |
147 | end | |
148 | end | |
149 | end | |
150 | end |
0 | require 'spec_helper' | |
1 | ||
2 | module Capistrano | |
3 | module DSL | |
4 | ||
5 | class DummyPaths | |
6 | include Paths | |
7 | end | |
8 | ||
9 | describe Paths do | |
10 | let(:paths) { DummyPaths.new } | |
11 | let(:parent) { Pathname.new('/var/shared') } | |
12 | ||
13 | let(:linked_dirs) { %w{log public/system} } | |
14 | let(:linked_files) { %w{config/database.yml log/my.log} } | |
15 | ||
16 | ||
17 | describe '#linked_dirs' do | |
18 | subject { paths.linked_dirs(parent) } | |
19 | ||
20 | before do | |
21 | paths.expects(:fetch).with(:linked_dirs).returns(linked_dirs) | |
22 | end | |
23 | ||
24 | it 'returns the full pathnames' do | |
25 | expect(subject).to eq [Pathname.new('/var/shared/log'), Pathname.new('/var/shared/public/system')] | |
26 | end | |
27 | end | |
28 | ||
29 | ||
30 | describe '#linked_files' do | |
31 | subject { paths.linked_files(parent) } | |
32 | ||
33 | before do | |
34 | paths.expects(:fetch).with(:linked_files).returns(linked_files) | |
35 | end | |
36 | ||
37 | it 'returns the full pathnames' do | |
38 | expect(subject).to eq [Pathname.new('/var/shared/config/database.yml'), Pathname.new('/var/shared/log/my.log')] | |
39 | end | |
40 | end | |
41 | ||
42 | describe '#linked_file_dirs' do | |
43 | subject { paths.linked_file_dirs(parent) } | |
44 | ||
45 | before do | |
46 | paths.expects(:fetch).with(:linked_files).returns(linked_files) | |
47 | end | |
48 | ||
49 | it 'returns the full paths names of the parent dirs' do | |
50 | expect(subject).to eq [Pathname.new('/var/shared/config'), Pathname.new('/var/shared/log')] | |
51 | end | |
52 | end | |
53 | ||
54 | describe '#linked_dir_parents' do | |
55 | subject { paths.linked_dir_parents(parent) } | |
56 | ||
57 | before do | |
58 | paths.expects(:fetch).with(:linked_dirs).returns(linked_dirs) | |
59 | end | |
60 | ||
61 | it 'returns the full paths names of the parent dirs' do | |
62 | expect(subject).to eq [Pathname.new('/var/shared'), Pathname.new('/var/shared/public')] | |
63 | end | |
64 | end | |
65 | ||
66 | end | |
67 | end | |
68 | end |
0 | require 'spec_helper' | |
1 | ||
2 | module Capistrano | |
3 | ||
4 | class DummyDSL | |
5 | include DSL | |
6 | end | |
7 | ||
8 | # see also - spec/integration/dsl_spec.rb | |
9 | describe DSL do | |
10 | let(:dsl) { DummyDSL.new } | |
11 | ||
12 | describe '#t' do | |
13 | before do | |
14 | I18n.expects(:t).with(:phrase, {count: 2, scope: :capistrano}) | |
15 | end | |
16 | ||
17 | it 'delegates to I18n' do | |
18 | dsl.t(:phrase, count: 2) | |
19 | end | |
20 | end | |
21 | ||
22 | describe '#stage_set?' do | |
23 | subject { dsl.stage_set? } | |
24 | ||
25 | context 'stage is set' do | |
26 | before do | |
27 | dsl.set(:stage, :sandbox) | |
28 | end | |
29 | it { should be_true } | |
30 | end | |
31 | ||
32 | context 'stage is not set' do | |
33 | before do | |
34 | dsl.set(:stage, nil) | |
35 | end | |
36 | it { should be_false } | |
37 | end | |
38 | end | |
39 | ||
40 | describe '#sudo' do | |
41 | ||
42 | before do | |
43 | dsl.expects(:execute).with(:sudo, :my, :command) | |
44 | end | |
45 | ||
46 | it 'prepends sudo, delegates to execute' do | |
47 | dsl.sudo(:my, :command) | |
48 | end | |
49 | end | |
50 | end | |
51 | end |
0 | require 'spec_helper' | |
1 | ||
2 | require 'capistrano/git' | |
3 | ||
4 | module Capistrano | |
5 | describe Git do | |
6 | let(:context) { Class.new.new } | |
7 | subject { Capistrano::Git.new(context, Capistrano::Git::DefaultStrategy) } | |
8 | ||
9 | describe "#git" do | |
10 | it "should call execute git in the context, with arguments" do | |
11 | context.expects(:execute).with(:git, :init) | |
12 | subject.git(:init) | |
13 | end | |
14 | end | |
15 | end | |
16 | ||
17 | describe Git::DefaultStrategy do | |
18 | let(:context) { Class.new.new } | |
19 | subject { Capistrano::Git.new(context, Capistrano::Git::DefaultStrategy) } | |
20 | ||
21 | describe "#test" do | |
22 | it "should call test for repo HEAD" do | |
23 | context.expects(:repo_path).returns("/path/to/repo") | |
24 | context.expects(:test).with " [ -f /path/to/repo/HEAD ] " | |
25 | ||
26 | subject.test | |
27 | end | |
28 | end | |
29 | ||
30 | describe "#check" do | |
31 | it "should test the repo url" do | |
32 | context.expects(:repo_url).returns(:url) | |
33 | context.expects(:test).with(:git, :'ls-remote -h', :url).returns(true) | |
34 | ||
35 | subject.check | |
36 | end | |
37 | end | |
38 | ||
39 | describe "#clone" do | |
40 | it "should run git clone" do | |
41 | context.expects(:repo_url).returns(:url) | |
42 | context.expects(:repo_path).returns(:path) | |
43 | ||
44 | context.expects(:execute).with(:git, :clone, '--mirror', :url, :path) | |
45 | ||
46 | subject.clone | |
47 | end | |
48 | end | |
49 | ||
50 | describe "#update" do | |
51 | it "should run git update" do | |
52 | context.expects(:execute).with(:git, :remote, :update) | |
53 | ||
54 | subject.update | |
55 | end | |
56 | end | |
57 | ||
58 | describe "#release" do | |
59 | it "should run git archive" do | |
60 | context.expects(:fetch).returns(:branch) | |
61 | context.expects(:release_path).returns(:path) | |
62 | ||
63 | context.expects(:execute).with(:git, :archive, :branch, '| tar -x -C', :path) | |
64 | ||
65 | subject.release | |
66 | end | |
67 | end | |
68 | end | |
69 | end |
0 | require 'spec_helper' | |
1 | ||
2 | require 'capistrano/hg' | |
3 | ||
4 | module Capistrano | |
5 | describe Hg do | |
6 | let(:context) { Class.new.new } | |
7 | subject { Capistrano::Hg.new(context, Capistrano::Hg::DefaultStrategy) } | |
8 | ||
9 | describe "#hg" do | |
10 | it "should call execute hg in the context, with arguments" do | |
11 | context.expects(:execute).with(:hg, :init) | |
12 | subject.hg(:init) | |
13 | end | |
14 | end | |
15 | end | |
16 | ||
17 | describe Hg::DefaultStrategy do | |
18 | let(:context) { Class.new.new } | |
19 | subject { Capistrano::Hg.new(context, Capistrano::Hg::DefaultStrategy) } | |
20 | ||
21 | describe "#test" do | |
22 | it "should call test for repo HEAD" do | |
23 | context.expects(:repo_path).returns("/path/to/repo") | |
24 | context.expects(:test).with " [ -d /path/to/repo/.hg ] " | |
25 | ||
26 | subject.test | |
27 | end | |
28 | end | |
29 | ||
30 | describe "#check" do | |
31 | it "should test the repo url" do | |
32 | context.expects(:repo_url).returns(:url) | |
33 | context.expects(:execute).with(:hg, "id", :url) | |
34 | ||
35 | subject.check | |
36 | end | |
37 | end | |
38 | ||
39 | describe "#clone" do | |
40 | it "should run hg clone" do | |
41 | context.expects(:repo_url).returns(:url) | |
42 | context.expects(:repo_path).returns(:path) | |
43 | ||
44 | context.expects(:execute).with(:hg, "clone", '--noupdate', :url, :path) | |
45 | ||
46 | subject.clone | |
47 | end | |
48 | end | |
49 | ||
50 | describe "#update" do | |
51 | it "should run hg update" do | |
52 | context.expects(:execute).with(:hg, "pull") | |
53 | ||
54 | subject.update | |
55 | end | |
56 | end | |
57 | ||
58 | describe "#release" do | |
59 | it "should run hg archive" do | |
60 | context.expects(:fetch).returns(:branch) | |
61 | context.expects(:release_path).returns(:path) | |
62 | ||
63 | context.expects(:execute).with(:hg, "archive", :path, "--rev", :branch) | |
64 | ||
65 | subject.release | |
66 | end | |
67 | end | |
68 | end | |
69 | end |
0 | require 'spec_helper' | |
1 | ||
2 | require 'capistrano/scm' | |
3 | ||
4 | module RaiseNotImplementedMacro | |
5 | def raise_not_implemented_on(method) | |
6 | it "should raise NotImplemented on #{method}" do | |
7 | expect { | |
8 | subject.send(method) | |
9 | }.to raise_error(NotImplementedError) | |
10 | end | |
11 | end | |
12 | end | |
13 | ||
14 | RSpec.configure do | |
15 | include RaiseNotImplementedMacro | |
16 | end | |
17 | ||
18 | module DummyStrategy | |
19 | def test | |
20 | test!("you dummy!") | |
21 | end | |
22 | end | |
23 | ||
24 | module BlindStrategy; end | |
25 | ||
26 | module Capistrano | |
27 | describe SCM do | |
28 | let(:context) { Class.new.new } | |
29 | ||
30 | describe "#initialize" do | |
31 | subject { Capistrano::SCM.new(context, DummyStrategy) } | |
32 | ||
33 | it "should load the provided strategy" do | |
34 | context.expects(:test).with("you dummy!") | |
35 | subject.test | |
36 | end | |
37 | end | |
38 | ||
39 | describe "Convenience methods" do | |
40 | subject { Capistrano::SCM.new(context, BlindStrategy) } | |
41 | ||
42 | describe "#test!" do | |
43 | it "should return call test on the context" do | |
44 | context.expects(:test).with(:x) | |
45 | subject.test!(:x) | |
46 | end | |
47 | end | |
48 | ||
49 | describe "#repo_url" do | |
50 | it "should return the repo url according to the context" do | |
51 | context.expects(:repo_url).returns(:url) | |
52 | subject.repo_url.should == :url | |
53 | end | |
54 | end | |
55 | ||
56 | describe "#repo_path" do | |
57 | it "should return the repo path according to the context" do | |
58 | context.expects(:repo_path).returns(:path) | |
59 | subject.repo_path.should == :path | |
60 | end | |
61 | end | |
62 | ||
63 | describe "#release_path" do | |
64 | it "should return the release path according to the context" do | |
65 | context.expects(:release_path).returns('/path/to/nowhere') | |
66 | subject.release_path.should == '/path/to/nowhere' | |
67 | end | |
68 | end | |
69 | ||
70 | describe "#fetch" do | |
71 | it "should call fetch on the context" do | |
72 | context.expects(:fetch) | |
73 | subject.fetch(:branch) | |
74 | end | |
75 | end | |
76 | end | |
77 | ||
78 | describe "With a 'blind' strategy" do | |
79 | subject { Capistrano::SCM.new(context, BlindStrategy) } | |
80 | ||
81 | describe "#test" do | |
82 | raise_not_implemented_on(:test) | |
83 | end | |
84 | ||
85 | describe "#check" do | |
86 | raise_not_implemented_on(:check) | |
87 | end | |
88 | ||
89 | describe "#clone" do | |
90 | raise_not_implemented_on(:clone) | |
91 | end | |
92 | ||
93 | describe "#update" do | |
94 | raise_not_implemented_on(:update) | |
95 | end | |
96 | ||
97 | describe "#release" do | |
98 | raise_not_implemented_on(:release) | |
99 | end | |
100 | end | |
101 | end | |
102 | end | |
103 |
0 | require 'spec_helper' | |
1 | ||
2 | require 'capistrano/svn' | |
3 | ||
4 | module Capistrano | |
5 | describe Svn do | |
6 | let(:context) { Class.new.new } | |
7 | subject { Capistrano::Svn.new(context, Capistrano::Svn::DefaultStrategy) } | |
8 | ||
9 | describe "#svn" do | |
10 | it "should call execute svn in the context, with arguments" do | |
11 | context.expects(:execute).with(:svn, :init) | |
12 | subject.svn(:init) | |
13 | end | |
14 | end | |
15 | end | |
16 | ||
17 | describe Svn::DefaultStrategy do | |
18 | let(:context) { Class.new.new } | |
19 | subject { Capistrano::Svn.new(context, Capistrano::Svn::DefaultStrategy) } | |
20 | ||
21 | describe "#test" do | |
22 | it "should call test for repo HEAD" do | |
23 | context.expects(:repo_path).returns("/path/to/repo") | |
24 | context.expects(:test).with " [ -d /path/to/repo/.svn ] " | |
25 | ||
26 | subject.test | |
27 | end | |
28 | end | |
29 | ||
30 | describe "#check" do | |
31 | it "should test the repo url" do | |
32 | context.expects(:repo_url).returns(:url) | |
33 | context.expects(:test).with(:svn, :info, :url).returns(true) | |
34 | ||
35 | subject.check | |
36 | end | |
37 | end | |
38 | ||
39 | describe "#clone" do | |
40 | it "should run svn checkout" do | |
41 | context.expects(:repo_url).returns(:url) | |
42 | context.expects(:repo_path).returns(:path) | |
43 | ||
44 | context.expects(:execute).with(:svn, :checkout, :url, :path) | |
45 | ||
46 | subject.clone | |
47 | end | |
48 | end | |
49 | ||
50 | describe "#update" do | |
51 | it "should run svn update" do | |
52 | context.expects(:execute).with(:svn, :update) | |
53 | ||
54 | subject.update | |
55 | end | |
56 | end | |
57 | ||
58 | describe "#release" do | |
59 | it "should run svn export" do | |
60 | context.expects(:release_path).returns(:path) | |
61 | ||
62 | context.expects(:execute).with(:svn, :export, '.', :path) | |
63 | ||
64 | subject.release | |
65 | end | |
66 | end | |
67 | end | |
68 | end |
0 | require 'spec_helper' | |
1 | ||
2 | module Capistrano | |
3 | ||
4 | describe VersionValidator do | |
5 | let(:validator) { VersionValidator.new(version) } | |
6 | let(:version) { stub } | |
7 | ||
8 | describe '#new' do | |
9 | it 'takes a version' do | |
10 | expect(validator) | |
11 | end | |
12 | end | |
13 | ||
14 | describe '#verify' do | |
15 | let(:current_version) { '3.0.1' } | |
16 | ||
17 | subject { validator.verify } | |
18 | ||
19 | before do | |
20 | validator.stubs(:current_version).returns(current_version) | |
21 | end | |
22 | ||
23 | context 'with exact version' do | |
24 | context 'valid' do | |
25 | let(:version) { '3.0.1' } | |
26 | it { should be_true } | |
27 | end | |
28 | ||
29 | context 'invalid - lower' do | |
30 | let(:version) { '3.0.0' } | |
31 | ||
32 | it 'fails' do | |
33 | expect { subject }.to raise_error | |
34 | end | |
35 | end | |
36 | ||
37 | context 'invalid - higher' do | |
38 | let(:version) { '3.0.2' } | |
39 | ||
40 | it 'fails' do | |
41 | expect { subject }.to raise_error | |
42 | end | |
43 | end | |
44 | ||
45 | end | |
46 | ||
47 | context 'with optimistic versioning' do | |
48 | context 'valid' do | |
49 | let(:version) { '>= 3.0.0' } | |
50 | it { should be_true } | |
51 | end | |
52 | ||
53 | context 'invalid - lower' do | |
54 | let(:version) { '<= 2.0.0' } | |
55 | ||
56 | it 'fails' do | |
57 | expect { subject }.to raise_error | |
58 | end | |
59 | end | |
60 | end | |
61 | ||
62 | ||
63 | ||
64 | context 'with pessimistic versioning' do | |
65 | context '2 decimal places' do | |
66 | context 'valid' do | |
67 | let(:version) { '~> 3.0.0' } | |
68 | it { should be_true } | |
69 | end | |
70 | ||
71 | context 'invalid' do | |
72 | let(:version) { '~> 3.1.0' } | |
73 | ||
74 | it 'fails' do | |
75 | expect { subject }.to raise_error | |
76 | end | |
77 | end | |
78 | end | |
79 | ||
80 | context '1 decimal place' do | |
81 | let(:current_version) { '3.5.0' } | |
82 | ||
83 | context 'valid' do | |
84 | let(:version) { '~> 3.1' } | |
85 | it { should be_true } | |
86 | end | |
87 | ||
88 | context 'invalid' do | |
89 | let(:version) { '~> 3.6' } | |
90 | it 'fails' do | |
91 | expect { subject }.to raise_error | |
92 | end | |
93 | end | |
94 | end | |
95 | ||
96 | end | |
97 | ||
98 | end | |
99 | ||
100 | end | |
101 | ||
102 | end |
0 | require 'spec_helper' | |
1 | ||
2 | module Capistrano | |
3 | ||
4 | describe Application do | |
5 | let(:app) { Application.new } | |
6 | end | |
7 | end |
0 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) | |
1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) | |
2 | require 'capistrano/all' | |
3 | require 'rspec' | |
4 | require 'mocha/api' | |
5 | ||
6 | # Requires supporting files with custom matchers and macros, etc, | |
7 | # in ./support/ and its subdirectories. | |
8 | Dir['#{File.dirname(__FILE__)}/support/**/*.rb'].each {|f| require f} | |
9 | ||
10 | RSpec.configure do |config| | |
11 | config.treat_symbols_as_metadata_keys_with_true_values = true | |
12 | config.mock_framework = :mocha | |
13 | config.order = 'random' | |
14 | end |
0 | .vagrant |
0 | Vagrant::Config.run do |config| | |
1 | ||
2 | [:app].each_with_index do |role, i| | |
3 | config.vm.define(role, primary: true) do |config| | |
4 | config.vm.box = role | |
5 | config.vm.box = 'precise64' | |
6 | config.vm.box_url = 'http://files.vagrantup.com/precise64.box' | |
7 | config.vm.forward_port 22, "222#{i}".to_i | |
8 | config.vm.provision :shell, inline: 'yes | sudo apt-get install git-core' | |
9 | end | |
10 | end | |
11 | ||
12 | end |
0 | RSpec::Matchers.define :be_a_symlink_to do |expected| | |
1 | match do |actual| | |
2 | File.identical?(expected, actual) | |
3 | end | |
4 | end |
0 | namespace :deploy do | |
1 | namespace :check do | |
2 | task :linked_files => 'config/database.yml' | |
3 | end | |
4 | end | |
5 | ||
6 | remote_file 'config/database.yml' => '/tmp/database.yml', roles: :all | |
7 | ||
8 | file '/tmp/database.yml' do |t| | |
9 | sh "touch #{t.name}" | |
10 | end |
0 | set :fail, proc { fail } | |
1 | before 'deploy:starting', :fail do | |
2 | on roles :all do | |
3 | execute :touch, shared_path.join('fail') | |
4 | end | |
5 | fetch(:fail) | |
6 | end |
0 | after 'deploy:failed', :failed do | |
1 | on roles :all do | |
2 | execute :touch, shared_path.join('failed') | |
3 | end | |
4 | end |
0 | require 'fileutils' | |
1 | module TestApp | |
2 | extend self | |
3 | ||
4 | def install | |
5 | install_test_app_with(default_config) | |
6 | end | |
7 | ||
8 | def default_config | |
9 | %{ | |
10 | set :deploy_to, '#{deploy_to}' | |
11 | set :repo_url, 'git://github.com/capistrano/capistrano.git' | |
12 | set :branch, 'master' | |
13 | set :ssh_options, { keys: "\#{ENV['HOME']}/.vagrant.d/insecure_private_key" } | |
14 | server 'vagrant@localhost:2220', roles: %w{web app} | |
15 | set :linked_files, #{linked_files} | |
16 | set :linked_dirs, #{linked_dirs} | |
17 | } | |
18 | end | |
19 | ||
20 | def linked_files | |
21 | %w{config/database.yml} | |
22 | end | |
23 | ||
24 | def linked_file | |
25 | shared_path.join(linked_files.first) | |
26 | end | |
27 | ||
28 | def linked_dirs | |
29 | %w{bin log public/system vendor/bundle} | |
30 | end | |
31 | ||
32 | def create_test_app | |
33 | FileUtils.rm_rf(test_app_path) | |
34 | FileUtils.mkdir(test_app_path) | |
35 | ||
36 | File.open(gemfile, 'w+') do |file| | |
37 | file.write "gem 'capistrano', path: '#{path_to_cap}'" | |
38 | end | |
39 | ||
40 | Dir.chdir(test_app_path) do | |
41 | %x[bundle] | |
42 | end | |
43 | end | |
44 | ||
45 | def install_test_app_with(config) | |
46 | create_test_app | |
47 | Dir.chdir(test_app_path) do | |
48 | %x[bundle exec cap install STAGES=#{stage}] | |
49 | end | |
50 | write_local_deploy_file(config) | |
51 | end | |
52 | ||
53 | def write_local_deploy_file(config) | |
54 | File.open(test_stage_path, 'w') do |file| | |
55 | file.write config | |
56 | end | |
57 | end | |
58 | ||
59 | def append_to_deploy_file(config) | |
60 | File.open(test_stage_path, 'a') do |file| | |
61 | file.write config + "\n" | |
62 | end | |
63 | end | |
64 | ||
65 | def prepend_to_capfile(config) | |
66 | current_capfile = File.read(capfile) | |
67 | File.open(capfile, 'w') do |file| | |
68 | file.write config | |
69 | file.write current_capfile | |
70 | end | |
71 | end | |
72 | ||
73 | def create_shared_directory(path) | |
74 | FileUtils.mkdir_p(shared_path.join(path)) | |
75 | end | |
76 | ||
77 | def create_shared_file(path) | |
78 | File.open(shared_path.join(path), 'w') | |
79 | end | |
80 | ||
81 | def cap(task) | |
82 | run "bundle exec cap #{stage} #{task}" | |
83 | end | |
84 | ||
85 | def run(command) | |
86 | output = nil | |
87 | Dir.chdir(test_app_path) do | |
88 | output = %x[#{command}] | |
89 | end | |
90 | [$?.success?, output] | |
91 | end | |
92 | ||
93 | def stage | |
94 | 'test' | |
95 | end | |
96 | ||
97 | def test_stage_path | |
98 | test_app_path.join('config/deploy/test.rb') | |
99 | end | |
100 | ||
101 | def test_app_path | |
102 | Pathname.new('/tmp/test_app') | |
103 | end | |
104 | ||
105 | def deploy_to | |
106 | Pathname.new('/home/vagrant/var/www/deploy') | |
107 | end | |
108 | ||
109 | def shared_path | |
110 | deploy_to.join('shared') | |
111 | end | |
112 | ||
113 | def current_path | |
114 | deploy_to.join('current') | |
115 | end | |
116 | ||
117 | def releases_path | |
118 | deploy_to.join('releases') | |
119 | end | |
120 | ||
121 | def release_path | |
122 | releases_path.join(timestamp) | |
123 | end | |
124 | ||
125 | def timestamp | |
126 | Time.now.utc.strftime("%Y%m%d%H%M%S") | |
127 | end | |
128 | ||
129 | def repo_path | |
130 | deploy_to.join('repo') | |
131 | end | |
132 | ||
133 | def path_to_cap | |
134 | File.expand_path('.') | |
135 | end | |
136 | ||
137 | def gemfile | |
138 | test_app_path.join('Gemfile') | |
139 | end | |
140 | ||
141 | def capfile | |
142 | test_app_path.join('Capfile') | |
143 | end | |
144 | ||
145 | def current_user | |
146 | `whoami`.chomp | |
147 | end | |
148 | ||
149 | def task_dir | |
150 | test_app_path.join('lib/capistrano/tasks') | |
151 | end | |
152 | ||
153 | def copy_task_to_test_app(source) | |
154 | FileUtils.cp(source, task_dir) | |
155 | end | |
156 | ||
157 | def config_path | |
158 | test_app_path.join('config') | |
159 | end | |
160 | ||
161 | def move_configuration_to_custom_location(location) | |
162 | prepend_to_capfile( | |
163 | %{ | |
164 | set :stage_config_path, "app/config/deploy" | |
165 | set :deploy_config_path, "app/config/deploy.rb" | |
166 | } | |
167 | ) | |
168 | ||
169 | location = test_app_path.join(location) | |
170 | FileUtils.mkdir_p(location) | |
171 | FileUtils.mv(config_path, location) | |
172 | end | |
173 | ||
174 | end |
0 | require "utils" | |
1 | require 'capistrano/cli/execute' | |
2 | ||
3 | class CLIExecuteTest < Test::Unit::TestCase | |
4 | class MockCLI | |
5 | attr_reader :options | |
6 | ||
7 | def initialize | |
8 | @options = {} | |
9 | end | |
10 | ||
11 | include Capistrano::CLI::Execute | |
12 | end | |
13 | ||
14 | def setup | |
15 | @cli = MockCLI.new | |
16 | @logger = stub_everything | |
17 | @config = stub(:logger => @logger, :debug= => nil, :dry_run= => nil, :preserve_roles= => nil) | |
18 | @config.stubs(:set) | |
19 | @config.stubs(:load) | |
20 | @config.stubs(:trigger) | |
21 | @cli.stubs(:instantiate_configuration).returns(@config) | |
22 | end | |
23 | ||
24 | def test_execute_should_set_logger_verbosity | |
25 | @cli.options[:verbose] = 7 | |
26 | @logger.expects(:level=).with(7) | |
27 | @cli.execute! | |
28 | end | |
29 | ||
30 | def test_execute_should_set_password | |
31 | @cli.options[:password] = "nosoup4u" | |
32 | @config.expects(:set).with(:password, "nosoup4u") | |
33 | @cli.execute! | |
34 | end | |
35 | ||
36 | def test_execute_should_set_prevars_before_loading | |
37 | @config.expects(:load).never | |
38 | @config.expects(:set).with(:stage, "foobar") | |
39 | @config.expects(:load).with("standard") | |
40 | @cli.options[:pre_vars] = { :stage => "foobar" } | |
41 | @cli.execute! | |
42 | end | |
43 | ||
44 | def test_execute_should_load_sysconf_if_sysconf_set_and_exists | |
45 | @cli.options[:sysconf] = "/etc/capistrano.conf" | |
46 | @config.expects(:load).with("/etc/capistrano.conf") | |
47 | File.expects(:file?).with("/etc/capistrano.conf").returns(true) | |
48 | @cli.execute! | |
49 | end | |
50 | ||
51 | def test_execute_should_not_load_sysconf_when_sysconf_set_and_not_exists | |
52 | @cli.options[:sysconf] = "/etc/capistrano.conf" | |
53 | File.expects(:file?).with("/etc/capistrano.conf").returns(false) | |
54 | @cli.execute! | |
55 | end | |
56 | ||
57 | def test_execute_should_load_dotfile_if_dotfile_set_and_exists | |
58 | @cli.options[:dotfile] = "/home/jamis/.caprc" | |
59 | @config.expects(:load).with("/home/jamis/.caprc") | |
60 | File.expects(:file?).with("/home/jamis/.caprc").returns(true) | |
61 | @cli.execute! | |
62 | end | |
63 | ||
64 | def test_execute_should_not_load_dotfile_when_dotfile_set_and_not_exists | |
65 | @cli.options[:dotfile] = "/home/jamis/.caprc" | |
66 | File.expects(:file?).with("/home/jamis/.caprc").returns(false) | |
67 | @cli.execute! | |
68 | end | |
69 | ||
70 | def test_execute_should_load_recipes_when_recipes_are_given | |
71 | @cli.options[:recipes] = %w(config/deploy path/to/extra) | |
72 | @config.expects(:load).with("config/deploy") | |
73 | @config.expects(:load).with("path/to/extra") | |
74 | @cli.execute! | |
75 | end | |
76 | ||
77 | def test_execute_should_set_vars_and_execute_tasks | |
78 | @cli.options[:vars] = { :foo => "bar", :baz => "bang" } | |
79 | @cli.options[:actions] = %w(first second) | |
80 | @config.expects(:set).with(:foo, "bar") | |
81 | @config.expects(:set).with(:baz, "bang") | |
82 | @config.expects(:find_and_execute_task).with("first", :before => :start, :after => :finish) | |
83 | @config.expects(:find_and_execute_task).with("second", :before => :start, :after => :finish) | |
84 | @cli.execute! | |
85 | end | |
86 | ||
87 | def test_execute_should_call_load_and_exit_triggers | |
88 | @cli.options[:actions] = %w(first second) | |
89 | @config.expects(:find_and_execute_task).with("first", :before => :start, :after => :finish) | |
90 | @config.expects(:find_and_execute_task).with("second", :before => :start, :after => :finish) | |
91 | @config.expects(:trigger).never | |
92 | @config.expects(:trigger).with(:load) | |
93 | @config.expects(:trigger).with(:exit) | |
94 | @cli.execute! | |
95 | end | |
96 | ||
97 | def test_execute_should_call_handle_error_when_exceptions_occur | |
98 | @config.expects(:load).raises(Exception, "boom") | |
99 | @cli.expects(:handle_error).with { |e,| Exception === e } | |
100 | @cli.execute! | |
101 | end | |
102 | ||
103 | def test_execute_should_return_config_instance | |
104 | assert_equal @config, @cli.execute! | |
105 | end | |
106 | ||
107 | def test_instantiate_configuration_should_return_new_configuration_instance | |
108 | assert_instance_of Capistrano::Configuration, MockCLI.new.instantiate_configuration | |
109 | end | |
110 | ||
111 | def test_handle_error_with_auth_error_should_abort_with_message_including_user_name | |
112 | @cli.expects(:abort).with { |s| s.include?("jamis") } | |
113 | @cli.handle_error(Net::SSH::AuthenticationFailed.new("jamis")) | |
114 | end | |
115 | ||
116 | def test_handle_error_with_cap_error_should_abort_with_message | |
117 | @cli.expects(:abort).with("Wish you were here") | |
118 | @cli.handle_error(Capistrano::Error.new("Wish you were here")) | |
119 | end | |
120 | ||
121 | def test_handle_error_with_other_errors_should_reraise_error | |
122 | other_error = Class.new(RuntimeError) | |
123 | assert_raises(other_error) { @cli.handle_error(other_error.new("boom")) } | |
124 | end | |
125 | ||
126 | def test_class_execute_method_should_call_parse_and_execute_with_ARGV | |
127 | cli = mock(:execute! => nil) | |
128 | MockCLI.expects(:parse).with(ARGV).returns(cli) | |
129 | MockCLI.execute | |
130 | end | |
131 | end |
0 | require "utils" | |
1 | require 'capistrano/cli/help' | |
2 | ||
3 | class CLIHelpTest < Test::Unit::TestCase | |
4 | class MockCLI | |
5 | attr_reader :options, :called_original | |
6 | ||
7 | def initialize | |
8 | @options = {} | |
9 | @called_original = false | |
10 | end | |
11 | ||
12 | def execute_requested_actions(config) | |
13 | @called_original = config | |
14 | end | |
15 | ||
16 | include Capistrano::CLI::Help | |
17 | end | |
18 | ||
19 | def setup | |
20 | @cli = MockCLI.new | |
21 | @cli.options[:verbose] = 0 | |
22 | @ui = stub("ui", :output_cols => 80, :output_rows => 20, :page_at= => nil) | |
23 | MockCLI.stubs(:ui).returns(@ui) | |
24 | end | |
25 | ||
26 | def test_execute_requested_actions_without_tasks_or_explain_should_call_original | |
27 | @cli.execute_requested_actions(:config) | |
28 | @cli.expects(:task_list).never | |
29 | @cli.expects(:explain_task).never | |
30 | assert_equal :config, @cli.called_original | |
31 | end | |
32 | ||
33 | def test_execute_requested_actions_with_tasks_should_call_task_list | |
34 | @cli.options[:tasks] = true | |
35 | @cli.expects(:task_list).with(:config, true) | |
36 | @cli.expects(:explain_task).never | |
37 | @cli.execute_requested_actions(:config) | |
38 | assert !@cli.called_original | |
39 | end | |
40 | ||
41 | def test_execute_requested_actions_with_explain_should_call_explain_task | |
42 | @cli.options[:explain] = "deploy_with_niftiness" | |
43 | @cli.expects(:task_list).never | |
44 | @cli.expects(:explain_task).with(:config, "deploy_with_niftiness") | |
45 | @cli.execute_requested_actions(:config) | |
46 | assert !@cli.called_original | |
47 | end | |
48 | ||
49 | def test_task_list_with_no_tasks_should_emit_warning | |
50 | config = mock("config", :task_list => []) | |
51 | @cli.expects(:warn) | |
52 | @cli.task_list(config) | |
53 | end | |
54 | ||
55 | def test_task_list_should_query_all_tasks_in_all_namespaces | |
56 | expected_max_len = 80 - 3 - MockCLI::LINE_PADDING | |
57 | task_list = [task("c"), task("g", "c:g"), task("b", "c:b"), task("a")] | |
58 | task_list.each { |t| t.expects(:brief_description).with(expected_max_len).returns(t.fully_qualified_name) } | |
59 | ||
60 | config = mock("config") | |
61 | config.expects(:task_list).with(:all).returns(task_list) | |
62 | @cli.stubs(:puts) | |
63 | @cli.task_list(config) | |
64 | end | |
65 | ||
66 | def test_task_list_should_query_tasks_with_pattern | |
67 | expected_max_len = 80 - 3 - MockCLI::LINE_PADDING | |
68 | task_list = [task("g", "c:g"), task("b", "c:b")] | |
69 | task_list.each { |t| t.expects(:brief_description).with(expected_max_len).returns(t.fully_qualified_name)} | |
70 | ||
71 | config = mock("config") | |
72 | config.expects(:task_list).with(:all).once.returns(task_list) | |
73 | ||
74 | @cli.stubs(:puts) | |
75 | @cli.task_list(config, "c") | |
76 | end | |
77 | ||
78 | def test_task_list_should_query_for_all_tasks_when_pattern_doesnt_match | |
79 | expected_max_len = 80 - 3 - MockCLI::LINE_PADDING | |
80 | task_list = [task("g", "c:g"), task("b", "c:b")] | |
81 | task_list.each { |t| t.expects(:brief_description).with(expected_max_len).returns(t.fully_qualified_name) } | |
82 | ||
83 | config = mock("config") | |
84 | config.expects(:task_list).with(:all).times(2).returns(task_list) | |
85 | ||
86 | @cli.stubs(:warn) | |
87 | @cli.stubs(:puts) | |
88 | @cli.task_list(config, "z") | |
89 | end | |
90 | ||
91 | def test_task_list_should_never_use_less_than_MIN_MAX_LEN_chars_for_descriptions | |
92 | @ui.stubs(:output_cols).returns(20) | |
93 | t = task("c") | |
94 | t.expects(:brief_description).with(30).returns("hello") | |
95 | config = mock("config", :task_list => [t]) | |
96 | @cli.stubs(:puts) | |
97 | @cli.task_list(config) | |
98 | end | |
99 | ||
100 | def test_task_list_should_not_include_tasks_with_blank_description_or_internal_by_default | |
101 | t1 = task("c") | |
102 | t1.expects(:brief_description).returns("hello") | |
103 | t2 = task("d", "d", "[internal] howdy") | |
104 | t2.expects(:brief_description).never | |
105 | t3 = task("e", "e", "") | |
106 | t3.expects(:brief_description).never | |
107 | ||
108 | config = mock("config", :task_list => [t1, t2, t3]) | |
109 | @cli.stubs(:puts) | |
110 | @cli.expects(:puts).never.with { |s,| (s || "").include?("[internal]") || s =~ /#\s*$/ } | |
111 | @cli.task_list(config) | |
112 | end | |
113 | ||
114 | def test_task_list_should_include_tasks_with_blank_descriptions_and_internal_when_verbose | |
115 | t1 = task("c") | |
116 | t1.expects(:brief_description).returns("hello") | |
117 | t2 = task("d", "d", "[internal] howdy") | |
118 | t2.expects(:brief_description).returns("[internal] howdy") | |
119 | t3 = task("e", "e", "") | |
120 | t3.expects(:brief_description).returns("") | |
121 | ||
122 | config = mock("config", :task_list => [t1, t2, t3]) | |
123 | @cli.options[:verbose] = 1 | |
124 | @cli.stubs(:puts) | |
125 | @cli.expects(:puts).with { |s,| (s || "").include?("[internal]") || s =~ /#\s*$/ }.at_least_once | |
126 | @cli.task_list(config) | |
127 | end | |
128 | ||
129 | def test_explain_task_should_warn_if_task_does_not_exist | |
130 | config = mock("config", :find_task => nil) | |
131 | @cli.expects(:warn).with { |s,| s =~ /`deploy_with_niftiness'/ } | |
132 | @cli.explain_task(config, "deploy_with_niftiness") | |
133 | end | |
134 | ||
135 | def test_explain_task_with_task_that_has_no_description_should_emit_stub | |
136 | t = mock("task", :description => "") | |
137 | config = mock("config") | |
138 | config.expects(:find_task).with("deploy_with_niftiness").returns(t) | |
139 | @cli.stubs(:puts) | |
140 | @cli.expects(:puts).with("There is no description for this task.") | |
141 | @cli.explain_task(config, "deploy_with_niftiness") | |
142 | end | |
143 | ||
144 | def test_explain_task_with_task_should_format_description | |
145 | t = stub("task", :description => "line1\nline2\n\nline3") | |
146 | config = mock("config", :find_task => t) | |
147 | @cli.stubs(:puts) | |
148 | @cli.explain_task(config, "deploy_with_niftiness") | |
149 | end | |
150 | ||
151 | def test_long_help_should_load_and_format_help_txt_file | |
152 | File.expects(:dirname).returns "a/b/c" | |
153 | File.expects(:read).with("a/b/c/help.txt").returns("text") | |
154 | @ui.expects(:say).with("text\n") | |
155 | @cli.long_help | |
156 | end | |
157 | ||
158 | private | |
159 | ||
160 | def task(name, fqn=name, desc="a description") | |
161 | stub("task", :name => name, :fully_qualified_name => fqn, :description => desc) | |
162 | end | |
163 | ||
164 | end |
0 | require "utils" | |
1 | require 'capistrano/cli/options' | |
2 | ||
3 | class CLIOptionsTest < Test::Unit::TestCase | |
4 | class ExitException < Exception; end | |
5 | ||
6 | class MockCLI | |
7 | def initialize | |
8 | @args = [] | |
9 | end | |
10 | ||
11 | attr_reader :args | |
12 | ||
13 | include Capistrano::CLI::Options | |
14 | end | |
15 | ||
16 | def setup | |
17 | @cli = MockCLI.new | |
18 | end | |
19 | ||
20 | def test_parse_options_should_require_non_empty_args_list | |
21 | @cli.stubs(:warn) | |
22 | @cli.expects(:exit).raises(ExitException) | |
23 | assert_raises(ExitException) { @cli.parse_options! } | |
24 | end | |
25 | ||
26 | def test_parse_options_with_d_should_set_debug_option | |
27 | @cli.args << "-d" | |
28 | @cli.parse_options! | |
29 | assert @cli.options[:debug] | |
30 | end | |
31 | ||
32 | def test_parse_options_with_n_should_set_dry_run_option | |
33 | @cli.args << "-n" | |
34 | @cli.parse_options! | |
35 | assert @cli.options[:dry_run] | |
36 | end | |
37 | ||
38 | def test_parse_options_with_dry_run_should_set_dry_run_option | |
39 | @cli.args << "--dry-run" | |
40 | @cli.parse_options! | |
41 | assert @cli.options[:dry_run] | |
42 | end | |
43 | ||
44 | def test_parse_options_with_e_should_set_explain_option | |
45 | @cli.args << "-e" << "sample" | |
46 | @cli.parse_options! | |
47 | assert_equal "sample", @cli.options[:explain] | |
48 | end | |
49 | ||
50 | def test_parse_options_with_f_should_add_recipe_file | |
51 | @cli.args << "-f" << "deploy" | |
52 | @cli.parse_options! | |
53 | assert_equal %w(deploy), @cli.options[:recipes] | |
54 | end | |
55 | ||
56 | def test_parse_options_with_multiple_f_should_add_each_as_recipe_file | |
57 | @cli.args << "-f" << "deploy" << "-f" << "monitor" | |
58 | @cli.parse_options! | |
59 | assert_equal %w(deploy monitor), @cli.options[:recipes] | |
60 | end | |
61 | ||
62 | def test_parse_options_with_H_should_show_verbose_help_and_exit | |
63 | @cli.expects(:exit).raises(ExitException) | |
64 | @cli.expects(:long_help) | |
65 | @cli.args << "-H" | |
66 | assert_raises(ExitException) { @cli.parse_options! } | |
67 | end | |
68 | ||
69 | def test_parse_options_with_h_should_show_options_and_exit | |
70 | @cli.expects(:puts).with(@cli.option_parser) | |
71 | @cli.expects(:exit).raises(ExitException) | |
72 | @cli.args << "-h" | |
73 | assert_raises(ExitException) { @cli.parse_options! } | |
74 | end | |
75 | ||
76 | def test_parse_options_with_p_should_prompt_for_password | |
77 | MockCLI.expects(:password_prompt).returns(:the_password) | |
78 | @cli.args << "-p" | |
79 | @cli.parse_options! | |
80 | assert_equal :the_password, @cli.options[:password] | |
81 | end | |
82 | ||
83 | def test_parse_options_without_p_should_set_proc_for_password | |
84 | @cli.args << "-e" << "sample" | |
85 | @cli.parse_options! | |
86 | assert_instance_of Proc, @cli.options[:password] | |
87 | end | |
88 | ||
89 | def test_parse_options_with_q_should_set_verbose_to_0 | |
90 | @cli.args << "-q" | |
91 | @cli.parse_options! | |
92 | assert_equal 0, @cli.options[:verbose] | |
93 | end | |
94 | ||
95 | def test_parse_options_with_r_should_set_preserve_roles_option | |
96 | @cli.args << "-r" | |
97 | @cli.parse_options! | |
98 | assert @cli.options[:preserve_roles] | |
99 | end | |
100 | ||
101 | def test_parse_options_with_preserve_roles_should_set_preserve_roles_option | |
102 | @cli.args << "--preserve-roles" | |
103 | @cli.parse_options! | |
104 | assert @cli.options[:preserve_roles] | |
105 | end | |
106 | ||
107 | def test_parse_options_with_S_should_set_pre_vars | |
108 | @cli.args << "-S" << "foo=bar" | |
109 | @cli.parse_options! | |
110 | assert_equal "bar", @cli.options[:pre_vars][:foo] | |
111 | end | |
112 | ||
113 | def test_S_should_coerce_digits_to_integers | |
114 | @cli.args << "-S" << "foo=1234" | |
115 | @cli.parse_options! | |
116 | assert_equal 1234, @cli.options[:pre_vars][:foo] | |
117 | end | |
118 | ||
119 | def test_S_should_treat_quoted_integers_as_string | |
120 | @cli.args << "-S" << "foo=\"1234\"" | |
121 | @cli.parse_options! | |
122 | assert_equal "1234", @cli.options[:pre_vars][:foo] | |
123 | end | |
124 | ||
125 | def test_S_should_treat_digits_with_dot_as_floating_point | |
126 | @cli.args << "-S" << "foo=3.1415" | |
127 | @cli.parse_options! | |
128 | assert_equal 3.1415, @cli.options[:pre_vars][:foo] | |
129 | end | |
130 | ||
131 | def test_S_should_treat_true_as_boolean_true | |
132 | @cli.args << "-S" << "foo=true" | |
133 | @cli.parse_options! | |
134 | assert_equal true, @cli.options[:pre_vars][:foo] | |
135 | end | |
136 | ||
137 | def test_S_should_treat_false_as_boolean_false | |
138 | @cli.args << "-S" << "foo=false" | |
139 | @cli.parse_options! | |
140 | assert_equal false, @cli.options[:pre_vars][:foo] | |
141 | end | |
142 | ||
143 | def test_S_should_treat_nil_as_nil | |
144 | @cli.args << "-S" << "foo=nil" | |
145 | @cli.parse_options! | |
146 | assert_equal nil, @cli.options[:pre_vars][:foo] | |
147 | end | |
148 | ||
149 | def test_parse_options_with_s_should_set_vars | |
150 | @cli.args << "-s" << "foo=bar" | |
151 | @cli.parse_options! | |
152 | assert_equal "bar", @cli.options[:vars][:foo] | |
153 | end | |
154 | ||
155 | def test_s_should_coerce_digits_to_integers | |
156 | @cli.args << "-s" << "foo=1234" | |
157 | @cli.parse_options! | |
158 | assert_equal 1234, @cli.options[:vars][:foo] | |
159 | end | |
160 | ||
161 | def test_s_should_treat_quoted_integers_as_string | |
162 | @cli.args << "-s" << "foo=\"1234\"" | |
163 | @cli.parse_options! | |
164 | assert_equal "1234", @cli.options[:vars][:foo] | |
165 | end | |
166 | ||
167 | def test_s_should_treat_digits_with_dot_as_floating_point | |
168 | @cli.args << "-s" << "foo=3.1415" | |
169 | @cli.parse_options! | |
170 | assert_equal 3.1415, @cli.options[:vars][:foo] | |
171 | end | |
172 | ||
173 | def test_s_should_treat_true_as_boolean_true | |
174 | @cli.args << "-s" << "foo=true" | |
175 | @cli.parse_options! | |
176 | assert_equal true, @cli.options[:vars][:foo] | |
177 | end | |
178 | ||
179 | def test_s_should_treat_false_as_boolean_false | |
180 | @cli.args << "-s" << "foo=false" | |
181 | @cli.parse_options! | |
182 | assert_equal false, @cli.options[:vars][:foo] | |
183 | end | |
184 | ||
185 | def test_s_should_treat_nil_as_nil | |
186 | @cli.args << "-s" << "foo=nil" | |
187 | @cli.parse_options! | |
188 | assert_equal nil, @cli.options[:vars][:foo] | |
189 | end | |
190 | ||
191 | def test_parse_options_with_T_should_set_tasks_option_and_set_verbose_off | |
192 | @cli.args << "-T" | |
193 | @cli.parse_options! | |
194 | assert @cli.options[:tasks] | |
195 | assert_equal 0, @cli.options[:verbose] | |
196 | end | |
197 | ||
198 | def test_parse_options_with_V_should_show_version_and_exit | |
199 | @cli.args << "-V" | |
200 | @cli.expects(:puts).with { |s| s.include?(Capistrano::Version.to_s) } | |
201 | @cli.expects(:exit).raises(ExitException) | |
202 | assert_raises(ExitException) { @cli.parse_options! } | |
203 | end | |
204 | ||
205 | def test_parse_options_with_v_should_set_verbose_to_1 | |
206 | @cli.args << "-v" | |
207 | @cli.parse_options! | |
208 | assert_equal 1, @cli.options[:verbose] | |
209 | end | |
210 | ||
211 | def test_parse_options_with_multiple_v_should_set_verbose_accordingly | |
212 | @cli.args << "-vvvvvvv" | |
213 | @cli.parse_options! | |
214 | assert_equal 7, @cli.options[:verbose] | |
215 | end | |
216 | ||
217 | def test_parse_options_without_X_should_set_sysconf | |
218 | @cli.args << "-v" | |
219 | @cli.parse_options! | |
220 | assert @cli.options.key?(:sysconf) | |
221 | end | |
222 | ||
223 | def test_parse_options_with_X_should_unset_sysconf | |
224 | @cli.args << "-X" | |
225 | @cli.parse_options! | |
226 | assert !@cli.options.key?(:sysconf) | |
227 | end | |
228 | ||
229 | def test_parse_options_without_x_should_set_dotfile | |
230 | @cli.args << "-v" | |
231 | @cli.parse_options! | |
232 | assert @cli.options.key?(:dotfile) | |
233 | end | |
234 | ||
235 | def test_parse_options_with_x_should_unset_dotfile | |
236 | @cli.args << "-x" | |
237 | @cli.parse_options! | |
238 | assert !@cli.options.key?(:dotfile) | |
239 | end | |
240 | ||
241 | def test_parse_options_without_q_or_v_should_set_verbose_to_3 | |
242 | @cli.args << "-x" | |
243 | @cli.parse_options! | |
244 | assert_equal 3, @cli.options[:verbose] | |
245 | end | |
246 | ||
247 | def test_should_search_for_default_recipes_if_f_not_given | |
248 | @cli.expects(:look_for_default_recipe_file!) | |
249 | @cli.args << "-v" | |
250 | @cli.parse_options! | |
251 | end | |
252 | ||
253 | def test_should_not_search_for_default_recipes_if_f_given | |
254 | @cli.expects(:look_for_default_recipe_file!).never | |
255 | @cli.args << "-f" << "hello" | |
256 | @cli.parse_options! | |
257 | end | |
258 | ||
259 | def test_F_should_search_for_default_recipes_even_if_f_is_given | |
260 | @cli.expects(:look_for_default_recipe_file!) | |
261 | @cli.args << "-Ff" << "hello" | |
262 | @cli.parse_options! | |
263 | end | |
264 | ||
265 | def test_should_extract_env_vars_from_command_line | |
266 | assert_nil ENV["HELLO"] | |
267 | assert_nil ENV["ANOTHER"] | |
268 | ||
269 | @cli.args << "HELLO=world" << "hello" << "ANOTHER=value" | |
270 | @cli.parse_options! | |
271 | ||
272 | assert_equal "world", ENV["HELLO"] | |
273 | assert_equal "value", ENV["ANOTHER"] | |
274 | ensure | |
275 | ENV.delete("HELLO") | |
276 | ENV.delete("ANOTHER") | |
277 | end | |
278 | ||
279 | def test_remaining_args_should_be_added_to_actions_list | |
280 | @cli.args << "-v" << "HELLO=world" << "-f" << "foo" << "something" << "else" | |
281 | @cli.parse_options! | |
282 | assert_equal %w(something else), @cli.args | |
283 | ensure | |
284 | ENV.delete("HELLO") | |
285 | end | |
286 | ||
287 | def test_search_for_default_recipe_file_should_look_for_Capfile | |
288 | File.stubs(:file?).returns(false) | |
289 | File.expects(:file?).with("Capfile").returns(true) | |
290 | @cli.args << "-v" | |
291 | @cli.parse_options! | |
292 | assert_equal %w(Capfile), @cli.options[:recipes] | |
293 | end | |
294 | ||
295 | def test_search_for_default_recipe_file_should_look_for_capfile | |
296 | File.stubs(:file?).returns(false) | |
297 | File.expects(:file?).with("capfile").returns(true) | |
298 | @cli.args << "-v" | |
299 | @cli.parse_options! | |
300 | assert_equal %w(capfile), @cli.options[:recipes] | |
301 | end | |
302 | ||
303 | def test_search_for_default_recipe_should_hike_up_the_directory_tree_until_it_finds_default_recipe | |
304 | File.stubs(:file?).returns(false) | |
305 | File.expects(:file?).with("capfile").times(2).returns(false,true) | |
306 | Dir.expects(:pwd).times(3).returns(*%w(/bar/baz /bar/baz /bar)) | |
307 | Dir.expects(:chdir).with("..") | |
308 | @cli.args << "-v" | |
309 | @cli.parse_options! | |
310 | assert_equal %w(capfile), @cli.options[:recipes] | |
311 | end | |
312 | ||
313 | def test_search_for_default_recipe_should_halt_at_root_directory | |
314 | File.stubs(:file?).returns(false) | |
315 | Dir.expects(:pwd).times(7).returns(*%w(/bar/baz /bar/baz /bar /bar / / /)) | |
316 | Dir.expects(:chdir).with("..").times(3) | |
317 | Dir.expects(:chdir).with("/bar/baz") | |
318 | @cli.args << "-v" | |
319 | @cli.parse_options! | |
320 | assert @cli.options[:recipes].empty? | |
321 | end | |
322 | ||
323 | def test_parse_should_instantiate_new_cli_and_call_parse_options | |
324 | cli = mock("cli", :parse_options! => nil) | |
325 | MockCLI.expects(:new).with(%w(a b c)).returns(cli) | |
326 | assert_equal cli, MockCLI.parse(%w(a b c)) | |
327 | end | |
328 | end |
0 | require "utils" | |
1 | require 'capistrano/cli/ui' | |
2 | ||
3 | class CLIUITest < Test::Unit::TestCase | |
4 | class MockCLI | |
5 | include Capistrano::CLI::UI | |
6 | end | |
7 | ||
8 | def test_ui_should_return_highline_instance | |
9 | assert_instance_of HighLine, MockCLI.ui | |
10 | end | |
11 | ||
12 | def test_password_prompt_should_have_default_prompt_and_set_echo_false | |
13 | q = mock("question") | |
14 | q.expects(:echo=).with(false) | |
15 | ui = mock("ui") | |
16 | ui.expects(:ask).with("Password: ").yields(q).returns("sayuncle") | |
17 | MockCLI.expects(:ui).returns(ui) | |
18 | assert_equal "sayuncle", MockCLI.password_prompt | |
19 | end | |
20 | ||
21 | def test_password_prompt_with_custom_prompt_should_use_custom_prompt | |
22 | ui = mock("ui") | |
23 | ui.expects(:ask).with("Give the passphrase: ").returns("sayuncle") | |
24 | MockCLI.expects(:ui).returns(ui) | |
25 | assert_equal "sayuncle", MockCLI.password_prompt("Give the passphrase: ") | |
26 | end | |
27 | end⏎ |
0 | require "utils" | |
1 | require 'capistrano/cli' | |
2 | ||
3 | class CLI_Test < Test::Unit::TestCase | |
4 | def test_options_ui_and_help_modules_should_integrate_successfully_with_configuration | |
5 | cli = Capistrano::CLI.parse(%w(-T -x -X)) | |
6 | cli.expects(:puts).at_least_once | |
7 | cli.execute! | |
8 | end | |
9 | ||
10 | def test_options_and_execute_modules_should_integrate_successfully_with_configuration | |
11 | path = "#{File.dirname(__FILE__)}/fixtures/cli_integration.rb" | |
12 | cli = Capistrano::CLI.parse(%W(-x -X -q -f #{path} testing)) | |
13 | config = cli.execute! | |
14 | assert config[:testing_occurred] | |
15 | end | |
16 | end |
0 | require "utils" | |
1 | require 'capistrano/command' | |
2 | require 'capistrano/configuration' | |
3 | ||
4 | class CommandTest < Test::Unit::TestCase | |
5 | def test_command_should_open_channels_on_all_sessions | |
6 | s1, s2, s3 = mock_session, mock_session, mock_session | |
7 | assert_equal "ls", Capistrano::Command.new("ls", [s1, s2, s3]).tree.fallback.command | |
8 | end | |
9 | ||
10 | def test_command_with_newlines_should_be_properly_escaped | |
11 | cmd = Capistrano::Command.new("ls\necho", [mock_session]) | |
12 | assert_equal "ls\\\necho", cmd.tree.fallback.command | |
13 | end | |
14 | ||
15 | def test_command_with_windows_newlines_should_be_properly_escaped | |
16 | cmd = Capistrano::Command.new("ls\r\necho", [mock_session]) | |
17 | assert_equal "ls\\\necho", cmd.tree.fallback.command | |
18 | end | |
19 | ||
20 | def test_command_with_pty_should_request_pty_and_register_success_callback | |
21 | session = setup_for_extracting_channel_action(:request_pty, true) do |ch| | |
22 | ch.expects(:exec).with(%(sh -c 'ls')) | |
23 | end | |
24 | Capistrano::Command.new("ls", [session], :pty => true) | |
25 | end | |
26 | ||
27 | def test_command_with_env_key_should_have_environment_constructed_and_prepended | |
28 | session = setup_for_extracting_channel_action do |ch| | |
29 | ch.expects(:request_pty).never | |
30 | ch.expects(:exec).with(%(env FOO=bar sh -c 'ls')) | |
31 | end | |
32 | Capistrano::Command.new("ls", [session], :env => { "FOO" => "bar" }) | |
33 | end | |
34 | ||
35 | def test_env_with_symbolic_key_should_be_accepted_as_a_string | |
36 | session = setup_for_extracting_channel_action do |ch| | |
37 | ch.expects(:exec).with(%(env FOO=bar sh -c 'ls')) | |
38 | end | |
39 | Capistrano::Command.new("ls", [session], :env => { :FOO => "bar" }) | |
40 | end | |
41 | ||
42 | def test_env_as_string_should_be_substituted_in_directly | |
43 | session = setup_for_extracting_channel_action do |ch| | |
44 | ch.expects(:exec).with(%(env HOWDY=there sh -c 'ls')) | |
45 | end | |
46 | Capistrano::Command.new("ls", [session], :env => "HOWDY=there") | |
47 | end | |
48 | ||
49 | def test_env_with_symbolic_value_should_be_accepted_as_string | |
50 | session = setup_for_extracting_channel_action do |ch| | |
51 | ch.expects(:exec).with(%(env FOO=bar sh -c 'ls')) | |
52 | end | |
53 | Capistrano::Command.new("ls", [session], :env => { "FOO" => :bar }) | |
54 | end | |
55 | ||
56 | def test_env_value_should_be_escaped | |
57 | session = setup_for_extracting_channel_action do |ch| | |
58 | ch.expects(:exec).with(%(env FOO=(\\ \\\"bar\\\"\\ ) sh -c 'ls')) | |
59 | end | |
60 | Capistrano::Command.new("ls", [session], :env => { "FOO" => '( "bar" )' }) | |
61 | end | |
62 | ||
63 | def test_env_with_multiple_keys_should_chain_the_entries_together | |
64 | session = setup_for_extracting_channel_action do |ch| | |
65 | ch.expects(:exec).with do |command| | |
66 | command =~ /^env / && | |
67 | command =~ /\ba=b\b/ && | |
68 | command =~ /\bc=d\b/ && | |
69 | command =~ /\be=f\b/ && | |
70 | command =~ / sh -c 'ls'$/ | |
71 | end | |
72 | end | |
73 | Capistrano::Command.new("ls", [session], :env => { :a => :b, :c => :d, :e => :f }) | |
74 | end | |
75 | ||
76 | def test_open_channel_should_set_host_key_on_channel | |
77 | channel = nil | |
78 | session = setup_for_extracting_channel_action { |ch| channel = ch } | |
79 | Capistrano::Command.new("ls", [session]) | |
80 | assert_equal "capistrano", channel[:host] | |
81 | end | |
82 | ||
83 | def test_open_channel_should_set_options_key_on_channel | |
84 | channel = nil | |
85 | session = setup_for_extracting_channel_action { |ch| channel = ch } | |
86 | Capistrano::Command.new("ls", [session], :data => "here we go") | |
87 | assert_equal({ :data => 'here we go' }, channel[:options]) | |
88 | end | |
89 | ||
90 | def test_successful_channel_should_send_command | |
91 | session = setup_for_extracting_channel_action do |ch| | |
92 | ch.expects(:exec).with(%(sh -c 'ls')) | |
93 | end | |
94 | Capistrano::Command.new("ls", [session]) | |
95 | end | |
96 | ||
97 | def test_successful_channel_with_shell_option_should_send_command_via_specified_shell | |
98 | session = setup_for_extracting_channel_action do |ch| | |
99 | ch.expects(:exec).with(%(/bin/bash -c 'ls')) | |
100 | end | |
101 | Capistrano::Command.new("ls", [session], :shell => "/bin/bash") | |
102 | end | |
103 | ||
104 | def test_successful_channel_with_shell_false_should_send_command_without_shell | |
105 | session = setup_for_extracting_channel_action do |ch| | |
106 | ch.expects(:exec).with(%(echo `hostname`)) | |
107 | end | |
108 | Capistrano::Command.new("echo `hostname`", [session], :shell => false) | |
109 | end | |
110 | ||
111 | def test_successful_channel_should_send_data_if_data_key_is_present | |
112 | session = setup_for_extracting_channel_action do |ch| | |
113 | ch.expects(:exec).with(%(sh -c 'ls')) | |
114 | ch.expects(:send_data).with("here we go") | |
115 | end | |
116 | Capistrano::Command.new("ls", [session], :data => "here we go") | |
117 | end | |
118 | ||
119 | def test_unsuccessful_pty_request_should_close_channel | |
120 | session = setup_for_extracting_channel_action(:request_pty, false) do |ch| | |
121 | ch.expects(:close) | |
122 | end | |
123 | Capistrano::Command.new("ls", [session], :pty => true) | |
124 | end | |
125 | ||
126 | def test_on_data_should_invoke_callback_as_stdout | |
127 | session = setup_for_extracting_channel_action(:on_data, "hello") | |
128 | called = false | |
129 | Capistrano::Command.new("ls", [session]) do |ch, stream, data| | |
130 | called = true | |
131 | assert_equal :out, stream | |
132 | assert_equal "hello", data | |
133 | end | |
134 | assert called | |
135 | end | |
136 | ||
137 | def test_on_extended_data_should_invoke_callback_as_stderr | |
138 | session = setup_for_extracting_channel_action(:on_extended_data, 2, "hello") | |
139 | called = false | |
140 | Capistrano::Command.new("ls", [session]) do |ch, stream, data| | |
141 | called = true | |
142 | assert_equal :err, stream | |
143 | assert_equal "hello", data | |
144 | end | |
145 | assert called | |
146 | end | |
147 | ||
148 | def test_on_request_should_record_exit_status | |
149 | data = mock(:read_long => 5) | |
150 | channel = nil | |
151 | session = setup_for_extracting_channel_action([:on_request, "exit-status"], data) { |ch| channel = ch } | |
152 | Capistrano::Command.new("ls", [session]) | |
153 | assert_equal 5, channel[:status] | |
154 | end | |
155 | ||
156 | def test_on_close_should_set_channel_closed | |
157 | channel = nil | |
158 | session = setup_for_extracting_channel_action(:on_close) { |ch| channel = ch } | |
159 | Capistrano::Command.new("ls", [session]) | |
160 | assert channel[:closed] | |
161 | end | |
162 | ||
163 | def test_stop_should_close_all_open_channels | |
164 | sessions = [mock_session(new_channel(false)), | |
165 | mock_session(new_channel(true)), | |
166 | mock_session(new_channel(false))] | |
167 | ||
168 | cmd = Capistrano::Command.new("ls", sessions) | |
169 | cmd.stop! | |
170 | end | |
171 | ||
172 | def test_process_should_return_cleanly_if_all_channels_have_zero_exit_status | |
173 | sessions = [mock_session(new_channel(true, 0)), | |
174 | mock_session(new_channel(true, 0)), | |
175 | mock_session(new_channel(true, 0))] | |
176 | cmd = Capistrano::Command.new("ls", sessions) | |
177 | assert_nothing_raised { cmd.process! } | |
178 | end | |
179 | ||
180 | def test_process_should_raise_error_if_any_channel_has_non_zero_exit_status | |
181 | sessions = [mock_session(new_channel(true, 0)), | |
182 | mock_session(new_channel(true, 0)), | |
183 | mock_session(new_channel(true, 1))] | |
184 | cmd = Capistrano::Command.new("ls", sessions) | |
185 | assert_raises(Capistrano::CommandError) { cmd.process! } | |
186 | end | |
187 | ||
188 | def test_command_error_should_include_accessor_with_host_array | |
189 | sessions = [mock_session(new_channel(true, 0)), | |
190 | mock_session(new_channel(true, 0)), | |
191 | mock_session(new_channel(true, 1))] | |
192 | cmd = Capistrano::Command.new("ls", sessions) | |
193 | ||
194 | begin | |
195 | cmd.process! | |
196 | flunk "expected an exception to be raised" | |
197 | rescue Capistrano::CommandError => e | |
198 | assert e.respond_to?(:hosts) | |
199 | assert_equal %w(capistrano), e.hosts.map { |h| h.to_s } | |
200 | end | |
201 | end | |
202 | ||
203 | def test_process_should_loop_until_all_channels_are_closed | |
204 | new_channel = Proc.new do |times| | |
205 | ch = mock("channel") | |
206 | returns = [false] * (times-1) | |
207 | ch.stubs(:to_ary) | |
208 | ch.stubs(:[]).with(:closed).returns(*(returns + [true])) | |
209 | ch.expects(:[]).with(:status).returns(0) | |
210 | ch | |
211 | end | |
212 | ||
213 | sessions = [mock_session(new_channel[5]), | |
214 | mock_session(new_channel[10]), | |
215 | mock_session(new_channel[7])] | |
216 | cmd = Capistrano::Command.new("ls", sessions) | |
217 | assert_nothing_raised do | |
218 | cmd.process! | |
219 | end | |
220 | end | |
221 | ||
222 | def test_process_should_instantiate_command_and_process! | |
223 | cmd = mock("command", :process! => nil) | |
224 | Capistrano::Command.expects(:new).with("ls -l", %w(a b c), {:foo => "bar"}).returns(cmd) | |
225 | Capistrano::Command.process("ls -l", %w(a b c), :foo => "bar") | |
226 | end | |
227 | ||
228 | def test_process_with_host_placeholder_should_substitute_host_placeholder_with_each_host | |
229 | session = setup_for_extracting_channel_action do |ch| | |
230 | ch.expects(:exec).with(%(sh -c 'echo capistrano')) | |
231 | end | |
232 | Capistrano::Command.new("echo $CAPISTRANO:HOST$", [session]) | |
233 | end | |
234 | ||
235 | class MockConfig | |
236 | include Capistrano::Configuration::Roles | |
237 | end | |
238 | ||
239 | def test_hostroles_substitution | |
240 | @config = MockConfig.new | |
241 | @config.server "capistrano", :db, :worker | |
242 | server = @config.roles[:db].servers.first | |
243 | channel = {:server => server, :host => 'capistrano'} | |
244 | tree = Capistrano::Command::Tree.new(@config) { |t| t.else("echo $CAPISTRANO:HOSTROLES$") } | |
245 | result = Capistrano::Command.new(tree, []).send(:replace_placeholders, "echo $CAPISTRANO:HOSTROLES$", channel) | |
246 | assert result == "echo db,worker" || result == "echo worker,db" | |
247 | end | |
248 | ||
249 | def test_process_with_unknown_placeholder_should_not_replace_placeholder | |
250 | session = setup_for_extracting_channel_action do |ch| | |
251 | ch.expects(:exec).with(%(sh -c 'echo $CAPISTRANO:OTHER$')) | |
252 | end | |
253 | Capistrano::Command.new("echo $CAPISTRANO:OTHER$", [session]) | |
254 | end | |
255 | ||
256 | private | |
257 | ||
258 | def mock_session(channel=nil) | |
259 | stub('session', | |
260 | :open_channel => channel, | |
261 | :preprocess => true, | |
262 | :postprocess => true, | |
263 | :listeners => {}, | |
264 | :xserver => server("capistrano")) | |
265 | end | |
266 | ||
267 | class MockChannel < Hash | |
268 | def close | |
269 | end | |
270 | end | |
271 | ||
272 | def new_channel(closed, status=nil) | |
273 | ch = MockChannel.new | |
274 | ch.update({ :closed => closed, :host => "capistrano", :server => server("capistrano") }) | |
275 | ch[:status] = status if status | |
276 | ch.expects(:close) unless closed | |
277 | ch | |
278 | end | |
279 | ||
280 | def setup_for_extracting_channel_action(action=nil, *args) | |
281 | s = server("capistrano") | |
282 | session = mock("session", :xserver => s) | |
283 | ||
284 | channel = {} | |
285 | session.expects(:open_channel).yields(channel) | |
286 | ||
287 | channel.stubs(:on_data) | |
288 | channel.stubs(:on_extended_data) | |
289 | channel.stubs(:on_request) | |
290 | channel.stubs(:on_close) | |
291 | channel.stubs(:exec) | |
292 | channel.stubs(:send_data) | |
293 | ||
294 | if action | |
295 | action = Array(action) | |
296 | channel.expects(action.first).with(*action[1..-1]).yields(channel, *args) | |
297 | end | |
298 | ||
299 | yield channel if block_given? | |
300 | ||
301 | session | |
302 | end | |
303 | end |
0 | require "utils" | |
1 | require 'capistrano/configuration/actions/file_transfer' | |
2 | ||
3 | class ConfigurationActionsFileTransferTest < Test::Unit::TestCase | |
4 | class MockConfig | |
5 | include Capistrano::Configuration::Actions::FileTransfer | |
6 | attr_accessor :sessions, :dry_run | |
7 | end | |
8 | ||
9 | def setup | |
10 | @config = MockConfig.new | |
11 | @config.stubs(:logger).returns(stub_everything) | |
12 | end | |
13 | ||
14 | def test_put_should_delegate_to_upload | |
15 | @config.expects(:upload).with { |from, to, opts| | |
16 | from.string == "some data" && to == "test.txt" && opts == { :mode => 0777 } } | |
17 | @config.expects(:run).never | |
18 | @config.put("some data", "test.txt", :mode => 0777) | |
19 | end | |
20 | ||
21 | def test_get_should_delegate_to_download_with_once | |
22 | @config.expects(:download).with("testr.txt", "testl.txt", :foo => "bar", :once => true) | |
23 | @config.get("testr.txt", "testl.txt", :foo => "bar") | |
24 | end | |
25 | ||
26 | def test_upload_should_delegate_to_transfer | |
27 | @config.expects(:transfer).with(:up, "testl.txt", "testr.txt", :foo => "bar") | |
28 | @config.upload("testl.txt", "testr.txt", :foo => "bar") | |
29 | end | |
30 | ||
31 | def test_upload_without_mode_should_not_try_to_chmod | |
32 | @config.expects(:transfer).with(:up, "testl.txt", "testr.txt", :foo => "bar") | |
33 | @config.expects(:run).never | |
34 | @config.upload("testl.txt", "testr.txt", :foo => "bar") | |
35 | end | |
36 | ||
37 | def test_upload_with_mode_should_try_to_chmod | |
38 | @config.expects(:transfer).with(:up, "testl.txt", "testr.txt", :foo => "bar") | |
39 | @config.expects(:run).with("chmod 775 testr.txt", {:foo => "bar"}) | |
40 | @config.upload("testl.txt", "testr.txt", :mode => 0775, :foo => "bar") | |
41 | end | |
42 | ||
43 | def test_upload_with_symbolic_mode_should_try_to_chmod | |
44 | @config.expects(:transfer).with(:up, "testl.txt", "testr.txt", :foo => "bar") | |
45 | @config.expects(:run).with("chmod g+w testr.txt", {:foo => "bar"}) | |
46 | @config.upload("testl.txt", "testr.txt", :mode => "g+w", :foo => "bar") | |
47 | end | |
48 | ||
49 | def test_download_should_delegate_to_transfer | |
50 | @config.expects(:transfer).with(:down, "testr.txt", "testl.txt", :foo => "bar") | |
51 | @config.download("testr.txt", "testl.txt", :foo => "bar") | |
52 | end | |
53 | ||
54 | def test_transfer_should_invoke_transfer_on_matching_servers | |
55 | @config.sessions = { :a => 1, :b => 2, :c => 3, :d => 4 } | |
56 | @config.expects(:execute_on_servers).with(:foo => "bar").yields([:a, :b, :c]) | |
57 | Capistrano::Transfer.expects(:process).with(:up, "testl.txt", "testr.txt", [1,2,3], {:foo => "bar", :logger => @config.logger}) | |
58 | @config.transfer(:up, "testl.txt", "testr.txt", :foo => "bar") | |
59 | end | |
60 | end |
0 | require "utils" | |
1 | require 'capistrano/configuration/actions/inspect' | |
2 | ||
3 | class ConfigurationActionsInspectTest < Test::Unit::TestCase | |
4 | class MockConfig | |
5 | include Capistrano::Configuration::Actions::Inspect | |
6 | end | |
7 | ||
8 | def setup | |
9 | @config = MockConfig.new | |
10 | @config.stubs(:logger).returns(stub_everything) | |
11 | end | |
12 | ||
13 | def test_stream_should_pass_options_through_to_run | |
14 | @config.expects(:invoke_command).with("tail -f foo.log", :once => true) | |
15 | @config.stream("tail -f foo.log", :once => true) | |
16 | end | |
17 | ||
18 | def test_stream_should_emit_stdout_via_puts | |
19 | @config.expects(:invoke_command).yields(mock("channel"), :out, "something streamed") | |
20 | @config.expects(:puts).with("something streamed") | |
21 | @config.expects(:warn).never | |
22 | @config.stream("tail -f foo.log") | |
23 | end | |
24 | ||
25 | def test_stream_should_emit_stderr_via_warn | |
26 | ch = mock("channel") | |
27 | ch.expects(:[]).with(:server).returns(server("capistrano")) | |
28 | @config.expects(:invoke_command).yields(ch, :err, "something streamed") | |
29 | @config.expects(:puts).never | |
30 | @config.expects(:warn).with("[err :: capistrano] something streamed") | |
31 | @config.stream("tail -f foo.log") | |
32 | end | |
33 | ||
34 | def test_capture_should_pass_options_merged_with_once_to_run | |
35 | @config.expects(:invoke_command).with("hostname", :foo => "bar", :once => true) | |
36 | @config.capture("hostname", :foo => "bar") | |
37 | end | |
38 | ||
39 | def test_capture_with_stderr_should_emit_stderr_via_warn | |
40 | ch = mock("channel") | |
41 | ch.expects(:[]).with(:server).returns(server("capistrano")) | |
42 | @config.expects(:invoke_command).yields(ch, :err, "boom") | |
43 | @config.expects(:warn).with("[err :: capistrano] boom") | |
44 | @config.capture("hostname") | |
45 | end | |
46 | ||
47 | def test_capture_with_stdout_should_aggregate_and_return_stdout | |
48 | config_expects_invoke_command_to_loop_with(mock("channel"), "foo", "bar", "baz") | |
49 | assert_equal "foobarbaz", @config.capture("hostname") | |
50 | end | |
51 | ||
52 | private | |
53 | ||
54 | def config_expects_invoke_command_to_loop_with(channel, *output) | |
55 | class <<@config | |
56 | attr_accessor :script, :channel | |
57 | def invoke_command(*args) | |
58 | script.each { |item| yield channel, :out, item } | |
59 | end | |
60 | end | |
61 | @config.channel = channel | |
62 | @config.script = output | |
63 | end | |
64 | end |
0 | require "utils" | |
1 | require 'capistrano/configuration/actions/invocation' | |
2 | require 'capistrano/configuration/actions/file_transfer' | |
3 | ||
4 | class ConfigurationActionsInvocationTest < Test::Unit::TestCase | |
5 | class MockConfig | |
6 | attr_reader :options | |
7 | attr_accessor :debug | |
8 | attr_accessor :dry_run | |
9 | attr_accessor :preserve_roles | |
10 | attr_accessor :servers | |
11 | ||
12 | def initialize | |
13 | @options = {} | |
14 | @servers = [] | |
15 | end | |
16 | ||
17 | def [](*args) | |
18 | @options[*args] | |
19 | end | |
20 | ||
21 | def set(name, value) | |
22 | @options[name] = value | |
23 | end | |
24 | ||
25 | def fetch(*args) | |
26 | @options.fetch(*args) | |
27 | end | |
28 | ||
29 | def execute_on_servers(options = {}) | |
30 | yield @servers | |
31 | end | |
32 | ||
33 | include Capistrano::Configuration::Actions::Invocation | |
34 | include Capistrano::Configuration::Actions::FileTransfer | |
35 | end | |
36 | ||
37 | def setup | |
38 | @config = make_config | |
39 | @original_io_proc = MockConfig.default_io_proc | |
40 | end | |
41 | ||
42 | def teardown | |
43 | MockConfig.default_io_proc = @original_io_proc | |
44 | end | |
45 | ||
46 | def test_run_options_should_be_passed_to_execute_on_servers | |
47 | @config.expects(:execute_on_servers).with(:foo => "bar") | |
48 | @config.run "ls", :foo => "bar" | |
49 | end | |
50 | ||
51 | def test_run_will_return_if_dry_run | |
52 | @config.expects(:dry_run).returns(true) | |
53 | @config.expects(:execute_on_servers).never | |
54 | @config.run "ls", :foo => "bar" | |
55 | end | |
56 | ||
57 | def test_put_wont_transfer_if_dry_run | |
58 | config = make_config | |
59 | config.dry_run = true | |
60 | config.servers = %w[ foo ] | |
61 | config.expects(:execute_on_servers).never | |
62 | ::Capistrano::Transfer.expects(:process).never | |
63 | config.put "foo", "bar", :mode => 0644 | |
64 | end | |
65 | ||
66 | def test_add_default_command_options_should_return_bare_options_if_there_is_no_env_or_shell_specified | |
67 | assert_equal({:foo => "bar"}, @config.add_default_command_options(:foo => "bar")) | |
68 | end | |
69 | ||
70 | def test_add_default_command_options_should_merge_default_environment_as_env | |
71 | @config[:default_environment][:bang] = "baz" | |
72 | assert_equal({:foo => "bar", :env => { :bang => "baz" }}, @config.add_default_command_options(:foo => "bar")) | |
73 | end | |
74 | ||
75 | def test_add_default_command_options_should_merge_env_with_default_environment | |
76 | @config[:default_environment][:bang] = "baz" | |
77 | @config[:default_environment][:bacon] = "crunchy" | |
78 | assert_equal({:foo => "bar", :env => { :bang => "baz", :bacon => "chunky", :flip => "flop" }}, @config.add_default_command_options(:foo => "bar", :env => {:bacon => "chunky", :flip => "flop"})) | |
79 | end | |
80 | ||
81 | def test_add_default_command_options_should_use_default_shell_if_present | |
82 | @config.set :default_shell, "/bin/bash" | |
83 | assert_equal({:foo => "bar", :shell => "/bin/bash"}, @config.add_default_command_options(:foo => "bar")) | |
84 | end | |
85 | ||
86 | def test_add_default_command_options_should_use_default_shell_of_false_if_present | |
87 | @config.set :default_shell, false | |
88 | assert_equal({:foo => "bar", :shell => false}, @config.add_default_command_options(:foo => "bar")) | |
89 | end | |
90 | ||
91 | def test_add_default_command_options_should_use_shell_in_preference_of_default_shell | |
92 | @config.set :default_shell, "/bin/bash" | |
93 | assert_equal({:foo => "bar", :shell => "/bin/sh"}, @config.add_default_command_options(:foo => "bar", :shell => "/bin/sh")) | |
94 | end | |
95 | ||
96 | def test_default_io_proc_should_log_stdout_arguments_as_info | |
97 | ch = { :host => "capistrano", | |
98 | :server => server("capistrano"), | |
99 | :options => { :logger => mock("logger") } } | |
100 | ch[:options][:logger].expects(:info).with("data stuff", "out :: capistrano") | |
101 | MockConfig.default_io_proc[ch, :out, "data stuff"] | |
102 | end | |
103 | ||
104 | def test_default_io_proc_should_log_stderr_arguments_as_important | |
105 | ch = { :host => "capistrano", | |
106 | :server => server("capistrano"), | |
107 | :options => { :logger => mock("logger") } } | |
108 | ch[:options][:logger].expects(:important).with("data stuff", "err :: capistrano") | |
109 | MockConfig.default_io_proc[ch, :err, "data stuff"] | |
110 | end | |
111 | ||
112 | def test_sudo_should_default_to_sudo | |
113 | @config.expects(:run).with("sudo -p 'sudo password: ' ls", {}) | |
114 | @config.sudo "ls" | |
115 | end | |
116 | ||
117 | def test_sudo_should_use_sudo_variable_definition | |
118 | @config.expects(:run).with("/opt/local/bin/sudo -p 'sudo password: ' ls", {}) | |
119 | @config.options[:sudo] = "/opt/local/bin/sudo" | |
120 | @config.sudo "ls" | |
121 | end | |
122 | ||
123 | def test_sudo_should_interpret_as_option_as_user | |
124 | @config.expects(:run).with("sudo -p 'sudo password: ' -u app ls", {}) | |
125 | @config.sudo "ls", :as => "app" | |
126 | end | |
127 | ||
128 | def test_sudo_should_pass_options_through_to_run | |
129 | @config.expects(:run).with("sudo -p 'sudo password: ' ls", :foo => "bar") | |
130 | @config.sudo "ls", :foo => "bar" | |
131 | end | |
132 | ||
133 | def test_sudo_should_avoid_minus_p_when_sudo_prompt_is_empty | |
134 | @config.set :sudo_prompt, "" | |
135 | @config.expects(:run).with("sudo ls", {}) | |
136 | @config.sudo "ls" | |
137 | end | |
138 | ||
139 | def test_sudo_should_interpret_sudo_prompt_variable_as_custom_prompt | |
140 | @config.set :sudo_prompt, "give it to me: " | |
141 | @config.expects(:run).with("sudo -p 'give it to me: ' ls", {}) | |
142 | @config.sudo "ls" | |
143 | end | |
144 | ||
145 | def test_sudo_behavior_callback_should_send_password_when_prompted_with_default_sudo_prompt | |
146 | ch = mock("channel") | |
147 | ch.expects(:send_data).with("g00b3r\n") | |
148 | @config.options[:password] = "g00b3r" | |
149 | @config.sudo_behavior_callback(nil)[ch, nil, "sudo password: "] | |
150 | end | |
151 | ||
152 | def test_sudo_behavior_callback_should_send_password_when_prompted_with_custom_sudo_prompt | |
153 | ch = mock("channel") | |
154 | ch.expects(:send_data).with("g00b3r\n") | |
155 | @config.set :sudo_prompt, "give it to me: " | |
156 | @config.options[:password] = "g00b3r" | |
157 | @config.sudo_behavior_callback(nil)[ch, nil, "give it to me: "] | |
158 | end | |
159 | ||
160 | def test_sudo_behavior_callback_with_incorrect_password_on_first_prompt | |
161 | ch = mock("channel") | |
162 | ch.stubs(:[]).with(:host).returns("capistrano") | |
163 | ch.stubs(:[]).with(:server).returns(server("capistrano")) | |
164 | @config.expects(:reset!).with(:password) | |
165 | @config.sudo_behavior_callback(nil)[ch, nil, "Sorry, try again."] | |
166 | end | |
167 | ||
168 | def test_sudo_behavior_callback_with_incorrect_password_on_subsequent_prompts | |
169 | callback = @config.sudo_behavior_callback(nil) | |
170 | ||
171 | ch = mock("channel") | |
172 | ch.stubs(:[]).with(:host).returns("capistrano") | |
173 | ch.stubs(:[]).with(:server).returns(server("capistrano")) | |
174 | ch2 = mock("channel") | |
175 | ch2.stubs(:[]).with(:host).returns("cap2") | |
176 | ch2.stubs(:[]).with(:server).returns(server("cap2")) | |
177 | ||
178 | @config.expects(:reset!).with(:password).times(2) | |
179 | ||
180 | callback[ch, nil, "Sorry, try again."] | |
181 | callback[ch2, nil, "Sorry, try again."] # shouldn't call reset! | |
182 | callback[ch, nil, "Sorry, try again."] | |
183 | end | |
184 | ||
185 | def test_sudo_behavior_callback_should_reset_password_and_prompt_again_if_output_includes_both_cues | |
186 | ch = mock("channel") | |
187 | ch.stubs(:[]).with(:host).returns("capistrano") | |
188 | ch.stubs(:[]).with(:server).returns(server("capistrano")) | |
189 | ch.expects(:send_data, "password!\n").times(2) | |
190 | ||
191 | @config.set(:password, "password!") | |
192 | @config.expects(:reset!).with(:password) | |
193 | ||
194 | callback = @config.sudo_behavior_callback(nil) | |
195 | callback[ch, :out, "sudo password: "] | |
196 | callback[ch, :out, "Sorry, try again.\nsudo password: "] | |
197 | end | |
198 | ||
199 | def test_sudo_behavior_callback_should_defer_to_fallback_for_other_output | |
200 | callback = @config.sudo_behavior_callback(inspectable_proc) | |
201 | ||
202 | a = mock("channel", :called => true) | |
203 | b = mock("stream", :called => true) | |
204 | c = mock("data", :called => true) | |
205 | ||
206 | callback[a, b, c] | |
207 | end | |
208 | ||
209 | def test_invoke_command_should_default_to_run | |
210 | @config.expects(:run).with("ls", :once => true) | |
211 | @config.invoke_command("ls", :once => true) | |
212 | end | |
213 | ||
214 | def test_invoke_command_should_delegate_to_method_identified_by_via | |
215 | @config.expects(:foobar).with("ls", :once => true) | |
216 | @config.invoke_command("ls", :once => true, :via => :foobar) | |
217 | end | |
218 | ||
219 | private | |
220 | ||
221 | def make_config | |
222 | config = MockConfig.new | |
223 | config.stubs(:logger).returns(stub_everything) | |
224 | config | |
225 | end | |
226 | ||
227 | def inspectable_proc | |
228 | Proc.new do |ch, stream, data| | |
229 | ch.called | |
230 | stream.called | |
231 | data.called | |
232 | end | |
233 | end | |
234 | ||
235 | def prepare_command(command, sessions, options) | |
236 | a = mock("channel", :called => true) | |
237 | b = mock("stream", :called => true) | |
238 | c = mock("data", :called => true) | |
239 | ||
240 | compare_args = Proc.new do |tree, sess, opts| | |
241 | tree.fallback.command == command && sess == sessions && opts == options | |
242 | end | |
243 | ||
244 | Capistrano::Command.expects(:process).with(&compare_args) | |
245 | end | |
246 | end |
0 | require 'utils' | |
1 | require 'capistrano/configuration/alias_task' | |
2 | require 'capistrano/configuration/execution' | |
3 | require 'capistrano/configuration/namespaces' | |
4 | require 'capistrano/task_definition' | |
5 | ||
6 | class AliasTaskTest < Test::Unit::TestCase | |
7 | class MockConfig | |
8 | attr_reader :options | |
9 | attr_accessor :logger | |
10 | ||
11 | def initialize(options={}) | |
12 | @options = {} | |
13 | @logger = options.delete(:logger) | |
14 | end | |
15 | ||
16 | include Capistrano::Configuration::AliasTask | |
17 | include Capistrano::Configuration::Execution | |
18 | include Capistrano::Configuration::Namespaces | |
19 | end | |
20 | ||
21 | def setup | |
22 | @config = MockConfig.new( :logger => stub(:debug => nil, :info => nil, :important => nil) ) | |
23 | end | |
24 | ||
25 | def test_makes_a_copy_of_the_task | |
26 | @config.task(:foo) { 42 } | |
27 | @config.alias_task 'new_foo', 'foo' | |
28 | ||
29 | assert @config.tasks.key?(:new_foo) | |
30 | end | |
31 | ||
32 | def test_original_task_remain_with_same_name | |
33 | @config.task(:foo) { 42 } | |
34 | @config.alias_task 'new_foo', 'foo' | |
35 | ||
36 | assert_equal :foo, @config.tasks[:foo].name | |
37 | assert_equal :new_foo, @config.tasks[:new_foo].name | |
38 | end | |
39 | ||
40 | def test_aliased_task_do_the_same | |
41 | @config.task(:foo) { 42 } | |
42 | @config.alias_task 'new_foo', 'foo' | |
43 | ||
44 | assert_equal 42, @config.find_and_execute_task('new_foo') | |
45 | end | |
46 | ||
47 | def test_aliased_task_should_preserve_description | |
48 | @config.task(:foo, :desc => "the Ultimate Question of Life, the Universe, and Everything" ) { 42 } | |
49 | @config.alias_task 'new_foo', 'foo' | |
50 | ||
51 | task = @config.find_task('foo') | |
52 | new_task = @config.find_task('new_foo') | |
53 | ||
54 | assert_equal task.description, new_task.description | |
55 | end | |
56 | ||
57 | def test_aliased_task_should_preserve_on_error | |
58 | @config.task(:foo, :on_error => :continue) { 42 } | |
59 | @config.alias_task 'new_foo', 'foo' | |
60 | ||
61 | task = @config.find_task('foo') | |
62 | new_task = @config.find_task('new_foo') | |
63 | ||
64 | assert_equal task.on_error, new_task.on_error | |
65 | end | |
66 | ||
67 | def test_aliased_task_should_preserve_max_hosts | |
68 | @config.task(:foo, :max_hosts => 5) { 42 } | |
69 | @config.alias_task 'new_foo', 'foo' | |
70 | ||
71 | task = @config.find_task('foo') | |
72 | new_task = @config.find_task('new_foo') | |
73 | ||
74 | assert_equal task.max_hosts, new_task.max_hosts | |
75 | end | |
76 | ||
77 | def test_raise_exception_when_task_doesnt_exist | |
78 | assert_raises(Capistrano::NoSuchTaskError) { @config.alias_task 'non_existant_task', 'fail_miserably' } | |
79 | end | |
80 | ||
81 | def test_convert_task_names_using_to_str | |
82 | @config.task(:foo, :role => :app) { 42 } | |
83 | ||
84 | @config.alias_task 'one', 'foo' | |
85 | @config.alias_task :two, 'foo' | |
86 | @config.alias_task 'three', :foo | |
87 | @config.alias_task :four, :foo | |
88 | ||
89 | assert @config.tasks.key?(:one) | |
90 | assert @config.tasks.key?(:two) | |
91 | assert @config.tasks.key?(:three) | |
92 | assert @config.tasks.key?(:four) | |
93 | end | |
94 | ||
95 | def test_raise_an_exception_when_task_names_can_not_be_converted | |
96 | @config.task(:foo, :role => :app) { 42 } | |
97 | ||
98 | assert_raises(ArgumentError) { @config.alias_task mock('x'), :foo } | |
99 | end | |
100 | ||
101 | def test_should_include_namespace | |
102 | @config.namespace(:outer) do | |
103 | task(:foo) { 42 } | |
104 | alias_task 'new_foo', 'foo' | |
105 | ||
106 | namespace(:inner) do | |
107 | task(:foo) { 43 } | |
108 | alias_task 'new_foo', 'foo' | |
109 | end | |
110 | end | |
111 | ||
112 | assert_equal 42, @config.find_and_execute_task('outer:new_foo') | |
113 | assert_equal 42, @config.find_and_execute_task('outer:foo') | |
114 | assert_equal 43, @config.find_and_execute_task('outer:inner:new_foo') | |
115 | assert_equal 43, @config.find_and_execute_task('outer:inner:foo') | |
116 | end | |
117 | end |
0 | require "utils" | |
1 | require 'capistrano/configuration/callbacks' | |
2 | ||
3 | class ConfigurationCallbacksTest < Test::Unit::TestCase | |
4 | class MockConfig | |
5 | attr_reader :original_initialize_called | |
6 | attr_reader :called | |
7 | ||
8 | def initialize | |
9 | @original_initialize_called = true | |
10 | @called = [] | |
11 | end | |
12 | ||
13 | def execute_task(task) | |
14 | invoke_task_directly(task) | |
15 | end | |
16 | ||
17 | protected | |
18 | ||
19 | def invoke_task_directly(task) | |
20 | @called << task | |
21 | end | |
22 | ||
23 | include Capistrano::Configuration::Callbacks | |
24 | end | |
25 | ||
26 | def setup | |
27 | @config = MockConfig.new | |
28 | @config.stubs(:logger).returns(stub_everything("logger")) | |
29 | end | |
30 | ||
31 | def test_initialize_should_initialize_callbacks_collection | |
32 | assert @config.original_initialize_called | |
33 | assert @config.callbacks.empty? | |
34 | end | |
35 | ||
36 | def test_before_should_delegate_to_on | |
37 | @config.expects(:on).with(:before, :foo, "bing:blang", {:only => :bar, :zip => :zing}) | |
38 | @config.before :bar, :foo, "bing:blang", :zip => :zing | |
39 | end | |
40 | ||
41 | def test_after_should_delegate_to_on | |
42 | @config.expects(:on).with(:after, :foo, "bing:blang", {:only => :bar, :zip => :zing}) | |
43 | @config.after :bar, :foo, "bing:blang", :zip => :zing | |
44 | end | |
45 | ||
46 | def test_on_with_single_reference_should_add_task_callback | |
47 | @config.on :before, :a_test | |
48 | assert_equal 1, @config.callbacks[:before].length | |
49 | assert_equal :a_test, @config.callbacks[:before][0].source | |
50 | @config.expects(:find_and_execute_task).with(:a_test) | |
51 | @config.callbacks[:before][0].call | |
52 | end | |
53 | ||
54 | def test_on_with_multi_reference_should_add_all_as_task_callback | |
55 | @config.on :before, :first, :second, :third | |
56 | assert_equal 3, @config.callbacks[:before].length | |
57 | assert_equal %w(first second third), @config.callbacks[:before].map { |c| c.source.to_s } | |
58 | end | |
59 | ||
60 | def test_on_with_block_should_add_block_as_proc_callback | |
61 | called = false | |
62 | @config.on(:before) { called = true } | |
63 | assert_equal 1, @config.callbacks[:before].length | |
64 | assert_instance_of Proc, @config.callbacks[:before][0].source | |
65 | @config.callbacks[:before][0].call | |
66 | assert called | |
67 | end | |
68 | ||
69 | def test_on_with_single_only_should_set_only_as_string_array_on_all_references | |
70 | @config.on :before, :first, "second:third", :only => :primary | |
71 | assert_equal 2, @config.callbacks[:before].length | |
72 | assert @config.callbacks[:before].all? { |c| c.only == %w(primary) } | |
73 | end | |
74 | ||
75 | def test_on_with_multi_only_should_set_only_as_string_array_on_all_references | |
76 | @config.on :before, :first, "second:third", :only => [:primary, "other:one"] | |
77 | assert_equal 2, @config.callbacks[:before].length | |
78 | assert @config.callbacks[:before].all? { |c| c.only == %w(primary other:one) } | |
79 | end | |
80 | ||
81 | def test_on_with_single_except_should_set_except_as_string_array_on_all_references | |
82 | @config.on :before, :first, "second:third", :except => :primary | |
83 | assert_equal 2, @config.callbacks[:before].length | |
84 | assert @config.callbacks[:before].all? { |c| c.except == %w(primary) } | |
85 | end | |
86 | ||
87 | def test_on_with_multi_except_should_set_except_as_string_array_on_all_references | |
88 | @config.on :before, :first, "second:third", :except => [:primary, "other:one"] | |
89 | assert_equal 2, @config.callbacks[:before].length | |
90 | assert @config.callbacks[:before].all? { |c| c.except == %w(primary other:one) } | |
91 | end | |
92 | ||
93 | def test_on_with_only_and_block_should_set_only_as_string_array | |
94 | @config.on(:before, :only => :primary) { blah } | |
95 | assert_equal 1, @config.callbacks[:before].length | |
96 | assert_equal %w(primary), @config.callbacks[:before].first.only | |
97 | end | |
98 | ||
99 | def test_on_with_except_and_block_should_set_except_as_string_array | |
100 | @config.on(:before, :except => :primary) { blah } | |
101 | assert_equal 1, @config.callbacks[:before].length | |
102 | assert_equal %w(primary), @config.callbacks[:before].first.except | |
103 | end | |
104 | ||
105 | def test_on_without_tasks_or_block_should_raise_error | |
106 | assert_raises(ArgumentError) { @config.on(:before) } | |
107 | end | |
108 | ||
109 | def test_on_with_both_tasks_and_block_should_raise_error | |
110 | assert_raises(ArgumentError) { @config.on(:before, :first) { blah } } | |
111 | end | |
112 | ||
113 | def test_trigger_without_constraints_should_invoke_all_callbacks | |
114 | task = stub(:fully_qualified_name => "any:old:thing") | |
115 | @config.on(:before, :first, "second:third") | |
116 | @config.on(:after, :another, "and:another") | |
117 | @config.expects(:find_and_execute_task).with(:first) | |
118 | @config.expects(:find_and_execute_task).with("second:third") | |
119 | @config.expects(:find_and_execute_task).with(:another).never | |
120 | @config.expects(:find_and_execute_task).with("and:another").never | |
121 | @config.trigger(:before, task) | |
122 | end | |
123 | ||
124 | def test_trigger_with_only_constraint_should_invoke_only_matching_callbacks | |
125 | task = stub(:fully_qualified_name => "any:old:thing") | |
126 | @config.on(:before, :first) | |
127 | @config.on(:before, "second:third", :only => "any:old:thing") | |
128 | @config.on(:before, "this:too", :only => "any:other:thing") | |
129 | @config.on(:after, :another, "and:another") | |
130 | @config.expects(:find_and_execute_task).with(:first) | |
131 | @config.expects(:find_and_execute_task).with("second:third") | |
132 | @config.expects(:find_and_execute_task).with("this:too").never | |
133 | @config.expects(:find_and_execute_task).with(:another).never | |
134 | @config.expects(:find_and_execute_task).with("and:another").never | |
135 | @config.trigger(:before, task) | |
136 | end | |
137 | ||
138 | def test_trigger_with_except_constraint_should_invoke_anything_but_matching_callbacks | |
139 | task = stub(:fully_qualified_name => "any:old:thing") | |
140 | @config.on(:before, :first) | |
141 | @config.on(:before, "second:third", :except => "any:old:thing") | |
142 | @config.on(:before, "this:too", :except => "any:other:thing") | |
143 | @config.on(:after, :another, "and:another") | |
144 | @config.expects(:find_and_execute_task).with(:first) | |
145 | @config.expects(:find_and_execute_task).with("second:third").never | |
146 | @config.expects(:find_and_execute_task).with("this:too") | |
147 | @config.expects(:find_and_execute_task).with(:another).never | |
148 | @config.expects(:find_and_execute_task).with("and:another").never | |
149 | @config.trigger(:before, task) | |
150 | end | |
151 | ||
152 | def test_trigger_without_task_should_invoke_all_callbacks_for_that_event | |
153 | task = stub(:fully_qualified_name => "any:old:thing") | |
154 | @config.on(:before, :first) | |
155 | @config.on(:before, "second:third", :except => "any:old:thing") | |
156 | @config.on(:before, "this:too", :except => "any:other:thing") | |
157 | @config.on(:after, :another, "and:another") | |
158 | @config.expects(:find_and_execute_task).with(:first) | |
159 | @config.expects(:find_and_execute_task).with("second:third") | |
160 | @config.expects(:find_and_execute_task).with("this:too") | |
161 | @config.expects(:find_and_execute_task).with(:another).never | |
162 | @config.expects(:find_and_execute_task).with("and:another").never | |
163 | @config.trigger(:before) | |
164 | end | |
165 | ||
166 | def test_execute_task_without_named_hooks_should_just_call_task | |
167 | ns = stub("namespace", :default_task => nil, :name => "old", :fully_qualified_name => "any:old") | |
168 | task = stub(:fully_qualified_name => "any:old:thing", :name => "thing", :namespace => ns) | |
169 | ||
170 | ns.stubs(:search_task).returns(nil) | |
171 | ||
172 | @config.execute_task(task) | |
173 | assert_equal [task], @config.called | |
174 | end | |
175 | ||
176 | end |
0 | require "utils" | |
1 | require 'capistrano/configuration/connections' | |
2 | ||
3 | class ConfigurationConnectionsTest < Test::Unit::TestCase | |
4 | class MockConfig | |
5 | attr_reader :original_initialize_called | |
6 | attr_reader :values | |
7 | attr_accessor :current_task | |
8 | ||
9 | def initialize | |
10 | @original_initialize_called = true | |
11 | @values = {} | |
12 | end | |
13 | ||
14 | def fetch(*args) | |
15 | @values.fetch(*args) | |
16 | end | |
17 | ||
18 | def [](key) | |
19 | @values[key] | |
20 | end | |
21 | ||
22 | def exists?(key) | |
23 | @values.key?(key) | |
24 | end | |
25 | ||
26 | include Capistrano::Configuration::Connections | |
27 | end | |
28 | ||
29 | def setup | |
30 | @config = MockConfig.new | |
31 | @config.stubs(:logger).returns(stub_everything) | |
32 | Net::SSH.stubs(:configuration_for).returns({}) | |
33 | @ssh_options = { | |
34 | :user => "user", | |
35 | :port => 8080, | |
36 | :password => "g00b3r", | |
37 | :ssh_options => { :debug => :verbose } | |
38 | } | |
39 | end | |
40 | ||
41 | def test_initialize_should_initialize_collections_and_call_original_initialize | |
42 | assert @config.original_initialize_called | |
43 | assert @config.sessions.empty? | |
44 | end | |
45 | ||
46 | def test_connection_factory_should_return_default_connection_factory_instance | |
47 | factory = @config.connection_factory | |
48 | assert_instance_of Capistrano::Configuration::Connections::DefaultConnectionFactory, factory | |
49 | end | |
50 | ||
51 | def test_connection_factory_instance_should_be_cached | |
52 | assert_same @config.connection_factory, @config.connection_factory | |
53 | end | |
54 | ||
55 | def test_default_connection_factory_honors_config_options | |
56 | server = server("capistrano") | |
57 | Capistrano::SSH.expects(:connect).with(server, @config).returns(:session) | |
58 | assert_equal :session, @config.connection_factory.connect_to(server) | |
59 | end | |
60 | ||
61 | def test_should_connect_through_gateway_if_gateway_variable_is_set | |
62 | @config.values[:gateway] = "j@gateway" | |
63 | Net::SSH::Gateway.expects(:new).with("gateway", "j", :password => nil, :auth_methods => %w(publickey hostbased), :config => false).returns(stub_everything) | |
64 | assert_instance_of Capistrano::Configuration::Connections::GatewayConnectionFactory, @config.connection_factory | |
65 | end | |
66 | ||
67 | def test_connection_factory_as_gateway_should_honor_config_options | |
68 | @config.values[:gateway] = "gateway" | |
69 | @config.values.update(@ssh_options) | |
70 | Net::SSH::Gateway.expects(:new).with("gateway", "user", :debug => :verbose, :port => 8080, :password => nil, :auth_methods => %w(publickey hostbased), :config => false).returns(stub_everything) | |
71 | assert_instance_of Capistrano::Configuration::Connections::GatewayConnectionFactory, @config.connection_factory | |
72 | end | |
73 | ||
74 | def test_connection_factory_as_gateway_should_chain_gateways_if_gateway_variable_is_an_array | |
75 | @config.values[:gateway] = ["j@gateway1", "k@gateway2"] | |
76 | gateway1 = mock | |
77 | Net::SSH::Gateway.expects(:new).with("gateway1", "j", :password => nil, :auth_methods => %w(publickey hostbased), :config => false).returns(gateway1) | |
78 | gateway1.expects(:open).returns(65535) | |
79 | Net::SSH::Gateway.expects(:new).with("127.0.0.1", "k", :port => 65535, :password => nil, :auth_methods => %w(publickey hostbased), :config => false).returns(stub_everything) | |
80 | assert_instance_of Capistrano::Configuration::Connections::GatewayConnectionFactory, @config.connection_factory | |
81 | end | |
82 | ||
83 | def test_connection_factory_as_gateway_should_chain_gateways_if_gateway_variable_is_a_hash | |
84 | @config.values[:gateway] = { ["j@gateway1", "k@gateway2"] => :default } | |
85 | gateway1 = mock | |
86 | Net::SSH::Gateway.expects(:new).with("gateway1", "j", :password => nil, :auth_methods => %w(publickey hostbased), :config => false).returns(gateway1) | |
87 | gateway1.expects(:open).returns(65535) | |
88 | Net::SSH::Gateway.expects(:new).with("127.0.0.1", "k", :port => 65535, :password => nil, :auth_methods => %w(publickey hostbased), :config => false).returns(stub_everything) | |
89 | assert_instance_of Capistrano::Configuration::Connections::GatewayConnectionFactory, @config.connection_factory | |
90 | end | |
91 | ||
92 | def test_connection_factory_as_gateway_should_share_gateway_between_connections | |
93 | @config.values[:gateway] = "j@gateway" | |
94 | Net::SSH::Gateway.expects(:new).once.with("gateway", "j", :password => nil, :auth_methods => %w(publickey hostbased), :config => false).returns(stub_everything) | |
95 | Capistrano::SSH.stubs(:connect).returns(stub_everything) | |
96 | assert_instance_of Capistrano::Configuration::Connections::GatewayConnectionFactory, @config.connection_factory | |
97 | @config.establish_connections_to(server("capistrano")) | |
98 | @config.establish_connections_to(server("another")) | |
99 | end | |
100 | ||
101 | def test_connection_factory_as_gateway_should_share_gateway_between_like_connections_if_gateway_variable_is_a_hash | |
102 | @config.values[:gateway] = { "j@gateway" => [ "capistrano", "another"] } | |
103 | Net::SSH::Gateway.expects(:new).once.with("gateway", "j", :password => nil, :auth_methods => %w(publickey hostbased), :config => false).returns(stub_everything) | |
104 | Capistrano::SSH.stubs(:connect).returns(stub_everything) | |
105 | assert_instance_of Capistrano::Configuration::Connections::GatewayConnectionFactory, @config.connection_factory | |
106 | @config.establish_connections_to(server("capistrano")) | |
107 | @config.establish_connections_to(server("another")) | |
108 | end | |
109 | ||
110 | def test_connection_factory_as_gateways_should_not_share_gateway_between_unlike_connections_if_gateway_variable_is_a_hash | |
111 | @config.values[:gateway] = { "j@gateway" => [ "capistrano", "another"], "k@gateway2" => "yafhost" } | |
112 | Net::SSH::Gateway.expects(:new).once.with("gateway", "j", :password => nil, :auth_methods => %w(publickey hostbased), :config => false).returns(stub_everything) | |
113 | Net::SSH::Gateway.expects(:new).once.with("gateway2", "k", :password => nil, :auth_methods => %w(publickey hostbased), :config => false).returns(stub_everything) | |
114 | Capistrano::SSH.stubs(:connect).returns(stub_everything) | |
115 | assert_instance_of Capistrano::Configuration::Connections::GatewayConnectionFactory, @config.connection_factory | |
116 | @config.establish_connections_to(server("capistrano")) | |
117 | @config.establish_connections_to(server("another")) | |
118 | @config.establish_connections_to(server("yafhost")) | |
119 | end | |
120 | ||
121 | def test_establish_connections_to_should_accept_a_single_nonarray_parameter | |
122 | Capistrano::SSH.expects(:connect).with { |s,| s.host == "capistrano" }.returns(:success) | |
123 | assert @config.sessions.empty? | |
124 | @config.establish_connections_to(server("capistrano")) | |
125 | assert_equal ["capistrano"], @config.sessions.keys.map(&:host) | |
126 | end | |
127 | ||
128 | def test_establish_connections_to_should_accept_an_array | |
129 | Capistrano::SSH.expects(:connect).times(3).returns(:success) | |
130 | assert @config.sessions.empty? | |
131 | @config.establish_connections_to(%w(cap1 cap2 cap3).map { |s| server(s) }) | |
132 | assert_equal %w(cap1 cap2 cap3), @config.sessions.keys.sort.map(&:host) | |
133 | end | |
134 | ||
135 | def test_establish_connections_to_should_not_attempt_to_reestablish_existing_connections | |
136 | Capistrano::SSH.expects(:connect).times(2).returns(:success) | |
137 | @config.sessions[server("cap1")] = :ok | |
138 | @config.establish_connections_to(%w(cap1 cap2 cap3).map { |s| server(s) }) | |
139 | assert_equal %w(cap1 cap2 cap3), @config.sessions.keys.sort.map(&:host) | |
140 | end | |
141 | ||
142 | def test_establish_connections_to_should_raise_one_connection_error_on_failure | |
143 | Capistrano::SSH.expects(:connect).times(2).raises(Exception) | |
144 | assert_raises(Capistrano::ConnectionError) { | |
145 | @config.establish_connections_to(%w(cap1 cap2).map { |s| server(s) }) | |
146 | } | |
147 | end | |
148 | ||
149 | def test_connection_error_should_include_accessor_with_host_array | |
150 | Capistrano::SSH.expects(:connect).times(2).raises(Exception) | |
151 | begin | |
152 | @config.establish_connections_to(%w(cap1 cap2).map { |s| server(s) }) | |
153 | flunk "expected an exception to be raised" | |
154 | rescue Capistrano::ConnectionError => e | |
155 | assert e.respond_to?(:hosts) | |
156 | assert_equal %w(cap1 cap2), e.hosts.map { |h| h.to_s }.sort | |
157 | end | |
158 | end | |
159 | ||
160 | def test_connection_error_should_only_include_failed_hosts | |
161 | Capistrano::SSH.expects(:connect).with(server('cap1'), anything).raises(Exception) | |
162 | Capistrano::SSH.expects(:connect).with(server('cap2'), anything).returns(:success) | |
163 | ||
164 | begin | |
165 | @config.establish_connections_to(%w(cap1 cap2).map { |s| server(s) }) | |
166 | flunk "expected an exception to be raised" | |
167 | rescue Capistrano::ConnectionError => e | |
168 | assert_equal %w(cap1), e.hosts.map { |h| h.to_s } | |
169 | end | |
170 | end | |
171 | ||
172 | def test_execute_on_servers_should_require_a_block | |
173 | assert_raises(ArgumentError) { @config.execute_on_servers } | |
174 | end | |
175 | ||
176 | def test_execute_on_servers_without_current_task_should_call_find_servers | |
177 | list = [server("first"), server("second")] | |
178 | @config.expects(:find_servers).with(:a => :b, :c => :d).returns(list) | |
179 | @config.expects(:establish_connections_to).with(list).returns(:done) | |
180 | @config.execute_on_servers(:a => :b, :c => :d) do |result| | |
181 | assert_equal list, result | |
182 | end | |
183 | end | |
184 | ||
185 | def test_execute_on_servers_without_current_task_should_raise_error_if_no_matching_servers | |
186 | @config.expects(:find_servers).with(:a => :b, :c => :d).returns([]) | |
187 | assert_raises(Capistrano::NoMatchingServersError) { @config.execute_on_servers(:a => :b, :c => :d) { |list| } } | |
188 | end | |
189 | ||
190 | def test_execute_on_servers_without_current_task_should_not_raise_error_if_no_matching_servers_and_continue_on_no_matching_servers | |
191 | @config.expects(:find_servers).with(:a => :b, :c => :d, :on_no_matching_servers => :continue).returns([]) | |
192 | assert_nothing_raised { @config.execute_on_servers(:a => :b, :c => :d, :on_no_matching_servers => :continue) { |list| } } | |
193 | end | |
194 | ||
195 | def test_execute_on_servers_should_raise_an_error_if_the_current_task_has_no_matching_servers_by_default | |
196 | @config.current_task = mock_task | |
197 | @config.expects(:find_servers_for_task).with(@config.current_task, {}).returns([]) | |
198 | assert_raises(Capistrano::NoMatchingServersError) do | |
199 | @config.execute_on_servers do | |
200 | flunk "should not get here" | |
201 | end | |
202 | end | |
203 | end | |
204 | ||
205 | def test_execute_on_servers_should_not_raise_an_error_if_the_current_task_has_no_matching_servers_by_default_and_continue_on_no_matching_servers | |
206 | @config.current_task = mock_task(:on_no_matching_servers => :continue) | |
207 | @config.expects(:find_servers_for_task).with(@config.current_task, {}).returns([]) | |
208 | assert_nothing_raised do | |
209 | @config.execute_on_servers do | |
210 | flunk "should not get here" | |
211 | end | |
212 | end | |
213 | end | |
214 | ||
215 | def test_execute_on_servers_should_not_raise_an_error_if_the_current_task_has_no_matching_servers_by_default_and_command_continues_on_no_matching_servers | |
216 | @config.current_task = mock_task | |
217 | @config.expects(:find_servers_for_task).with(@config.current_task, :on_no_matching_servers => :continue).returns([]) | |
218 | assert_nothing_raised do | |
219 | @config.execute_on_servers(:on_no_matching_servers => :continue) do | |
220 | flunk "should not get here" | |
221 | end | |
222 | end | |
223 | end | |
224 | ||
225 | def test_execute_on_servers_should_determine_server_list_from_active_task | |
226 | assert @config.sessions.empty? | |
227 | @config.current_task = mock_task | |
228 | @config.expects(:find_servers_for_task).with(@config.current_task, {}).returns([server("cap1"), server("cap2"), server("cap3")]) | |
229 | Capistrano::SSH.expects(:connect).times(3).returns(:success) | |
230 | @config.execute_on_servers {} | |
231 | assert_equal %w(cap1 cap2 cap3), @config.sessions.keys.sort.map { |s| s.host } | |
232 | end | |
233 | ||
234 | def test_execute_on_servers_should_yield_server_list_to_block | |
235 | assert @config.sessions.empty? | |
236 | @config.current_task = mock_task | |
237 | @config.expects(:find_servers_for_task).with(@config.current_task, {}).returns([server("cap1"), server("cap2"), server("cap3")]) | |
238 | Capistrano::SSH.expects(:connect).times(3).returns(:success) | |
239 | block_called = false | |
240 | @config.execute_on_servers do |servers| | |
241 | block_called = true | |
242 | assert servers.detect { |s| s.host == "cap1" } | |
243 | assert servers.detect { |s| s.host == "cap2" } | |
244 | assert servers.detect { |s| s.host == "cap3" } | |
245 | assert servers.all? { |s| @config.sessions[s] } | |
246 | end | |
247 | assert block_called | |
248 | end | |
249 | ||
250 | def test_execute_on_servers_with_once_option_should_establish_connection_to_and_yield_only_the_first_server | |
251 | assert @config.sessions.empty? | |
252 | @config.current_task = mock_task | |
253 | @config.expects(:find_servers_for_task).with(@config.current_task, :once => true).returns([server("cap1"), server("cap2"), server("cap3")]) | |
254 | Capistrano::SSH.expects(:connect).returns(:success) | |
255 | block_called = false | |
256 | @config.execute_on_servers(:once => true) do |servers| | |
257 | block_called = true | |
258 | assert_equal %w(cap1), servers.map { |s| s.host } | |
259 | end | |
260 | assert block_called | |
261 | assert_equal %w(cap1), @config.sessions.keys.sort.map { |s| s.host } | |
262 | end | |
263 | ||
264 | def test_execute_servers_should_raise_connection_error_on_failure_by_default | |
265 | @config.current_task = mock_task | |
266 | @config.expects(:find_servers_for_task).with(@config.current_task, {}).returns([server("cap1")]) | |
267 | Capistrano::SSH.expects(:connect).raises(Exception) | |
268 | assert_raises(Capistrano::ConnectionError) do | |
269 | @config.execute_on_servers do | |
270 | flunk "expected an exception to be raised" | |
271 | end | |
272 | end | |
273 | end | |
274 | ||
275 | def test_execute_servers_should_not_raise_connection_error_on_failure_with_on_errors_continue | |
276 | @config.current_task = mock_task(:on_error => :continue) | |
277 | @config.expects(:find_servers_for_task).with(@config.current_task, {}).returns([server("cap1"), server("cap2")]) | |
278 | Capistrano::SSH.expects(:connect).with(server('cap1'), anything).raises(Exception) | |
279 | Capistrano::SSH.expects(:connect).with(server('cap2'), anything).returns(:success) | |
280 | assert_nothing_raised { | |
281 | @config.execute_on_servers do |servers| | |
282 | assert_equal %w(cap2), servers.map { |s| s.host } | |
283 | end | |
284 | } | |
285 | end | |
286 | ||
287 | def test_execute_on_servers_should_not_try_to_connect_to_hosts_with_connection_errors_with_on_errors_continue | |
288 | list = [server("cap1"), server("cap2")] | |
289 | @config.current_task = mock_task(:on_error => :continue) | |
290 | @config.expects(:find_servers_for_task).with(@config.current_task, {}).returns(list) | |
291 | Capistrano::SSH.expects(:connect).with(server('cap1'), anything).raises(Exception) | |
292 | Capistrano::SSH.expects(:connect).with(server('cap2'), anything).returns(:success) | |
293 | @config.execute_on_servers do |servers| | |
294 | assert_equal %w(cap2), servers.map { |s| s.host } | |
295 | end | |
296 | @config.expects(:find_servers_for_task).with(@config.current_task, {}).returns(list) | |
297 | @config.execute_on_servers do |servers| | |
298 | assert_equal %w(cap2), servers.map { |s| s.host } | |
299 | end | |
300 | end | |
301 | ||
302 | def test_execute_on_servers_should_not_try_to_connect_to_hosts_with_command_errors_with_on_errors_continue | |
303 | cap1 = server("cap1") | |
304 | cap2 = server("cap2") | |
305 | @config.current_task = mock_task(:on_error => :continue) | |
306 | @config.expects(:find_servers_for_task).with(@config.current_task, {}).returns([cap1, cap2]) | |
307 | Capistrano::SSH.expects(:connect).times(2).returns(:success) | |
308 | @config.execute_on_servers do |servers| | |
309 | error = Capistrano::CommandError.new | |
310 | error.hosts = [cap1] | |
311 | raise error | |
312 | end | |
313 | @config.expects(:find_servers_for_task).with(@config.current_task, {}).returns([cap1, cap2]) | |
314 | @config.execute_on_servers do |servers| | |
315 | assert_equal %w(cap2), servers.map { |s| s.host } | |
316 | end | |
317 | end | |
318 | ||
319 | def test_execute_on_servers_should_not_try_to_connect_to_hosts_with_transfer_errors_with_on_errors_continue | |
320 | cap1 = server("cap1") | |
321 | cap2 = server("cap2") | |
322 | @config.current_task = mock_task(:on_error => :continue) | |
323 | @config.expects(:find_servers_for_task).with(@config.current_task, {}).returns([cap1, cap2]) | |
324 | Capistrano::SSH.expects(:connect).times(2).returns(:success) | |
325 | @config.execute_on_servers do |servers| | |
326 | error = Capistrano::TransferError.new | |
327 | error.hosts = [cap1] | |
328 | raise error | |
329 | end | |
330 | @config.expects(:find_servers_for_task).with(@config.current_task, {}).returns([cap1, cap2]) | |
331 | @config.execute_on_servers do |servers| | |
332 | assert_equal %w(cap2), servers.map { |s| s.host } | |
333 | end | |
334 | end | |
335 | ||
336 | def test_connect_should_establish_connections_to_all_servers_in_scope | |
337 | assert @config.sessions.empty? | |
338 | @config.current_task = mock_task | |
339 | @config.expects(:find_servers_for_task).with(@config.current_task, {}).returns([server("cap1"), server("cap2"), server("cap3")]) | |
340 | Capistrano::SSH.expects(:connect).times(3).returns(:success) | |
341 | @config.connect! | |
342 | assert_equal %w(cap1 cap2 cap3), @config.sessions.keys.sort.map { |s| s.host } | |
343 | end | |
344 | ||
345 | def test_execute_on_servers_should_only_run_on_tasks_max_hosts_hosts_at_once | |
346 | cap1 = server("cap1") | |
347 | cap2 = server("cap2") | |
348 | connection1 = mock() | |
349 | connection2 = mock() | |
350 | connection1.expects(:close) | |
351 | connection2.expects(:close) | |
352 | @config.current_task = mock_task(:max_hosts => 1) | |
353 | @config.expects(:find_servers_for_task).with(@config.current_task, {}).returns([cap1, cap2]) | |
354 | Capistrano::SSH.expects(:connect).times(2).returns(connection1).then.returns(connection2) | |
355 | block_called = 0 | |
356 | @config.execute_on_servers do |servers| | |
357 | block_called += 1 | |
358 | assert_equal 1, servers.size | |
359 | end | |
360 | assert_equal 2, block_called | |
361 | end | |
362 | ||
363 | def test_execute_on_servers_should_only_run_on_max_hosts_hosts_at_once | |
364 | cap1 = server("cap1") | |
365 | cap2 = server("cap2") | |
366 | connection1 = mock() | |
367 | connection2 = mock() | |
368 | connection1.expects(:close) | |
369 | connection2.expects(:close) | |
370 | @config.current_task = mock_task(:max_hosts => 1) | |
371 | @config.expects(:find_servers_for_task).with(@config.current_task, {}).returns([cap1, cap2]) | |
372 | Capistrano::SSH.expects(:connect).times(2).returns(connection1).then.returns(connection2) | |
373 | block_called = 0 | |
374 | @config.execute_on_servers do |servers| | |
375 | block_called += 1 | |
376 | assert_equal 1, servers.size | |
377 | end | |
378 | assert_equal 2, block_called | |
379 | end | |
380 | ||
381 | def test_execute_on_servers_should_cope_with_already_dropped_connections_when_attempting_to_close_them | |
382 | cap1 = server("cap1") | |
383 | cap2 = server("cap2") | |
384 | connection1 = mock() | |
385 | connection2 = mock() | |
386 | connection3 = mock() | |
387 | connection4 = mock() | |
388 | connection1.expects(:close).raises(IOError) | |
389 | connection2.expects(:close) | |
390 | connection3.expects(:close) | |
391 | connection4.expects(:close) | |
392 | @config.current_task = mock_task(:max_hosts => 1) | |
393 | @config.expects(:find_servers_for_task).times(2).with(@config.current_task, {}).returns([cap1, cap2]) | |
394 | Capistrano::SSH.expects(:connect).times(4).returns(connection1).then.returns(connection2).then.returns(connection3).then.returns(connection4) | |
395 | @config.execute_on_servers {} | |
396 | @config.execute_on_servers {} | |
397 | end | |
398 | ||
399 | def test_connect_should_honor_once_option | |
400 | assert @config.sessions.empty? | |
401 | @config.current_task = mock_task | |
402 | @config.expects(:find_servers_for_task).with(@config.current_task, :once => true).returns([server("cap1"), server("cap2"), server("cap3")]) | |
403 | Capistrano::SSH.expects(:connect).returns(:success) | |
404 | @config.connect! :once => true | |
405 | assert_equal %w(cap1), @config.sessions.keys.sort.map { |s| s.host } | |
406 | end | |
407 | ||
408 | private | |
409 | ||
410 | def mock_task(options={}) | |
411 | continue_on_error = options[:on_error] == :continue | |
412 | stub("task", | |
413 | :fully_qualified_name => "name", | |
414 | :options => options, | |
415 | :continue_on_error? => continue_on_error, | |
416 | :max_hosts => options[:max_hosts] | |
417 | ) | |
418 | end | |
419 | end |
0 | require "utils" | |
1 | require 'capistrano/configuration/execution' | |
2 | require 'capistrano/task_definition' | |
3 | ||
4 | class ConfigurationExecutionTest < Test::Unit::TestCase | |
5 | class MockConfig | |
6 | attr_reader :tasks, :namespaces, :fully_qualified_name, :parent | |
7 | attr_reader :state, :original_initialize_called | |
8 | attr_accessor :logger, :default_task | |
9 | ||
10 | def initialize(options={}) | |
11 | @original_initialize_called = true | |
12 | @tasks = {} | |
13 | @namespaces = {} | |
14 | @state = {} | |
15 | @fully_qualified_name = options[:fqn] | |
16 | @parent = options[:parent] | |
17 | @logger = options.delete(:logger) | |
18 | end | |
19 | ||
20 | include Capistrano::Configuration::Execution | |
21 | end | |
22 | ||
23 | def setup | |
24 | @config = MockConfig.new(:logger => stub(:debug => nil, :info => nil, :important => nil)) | |
25 | @config.stubs(:search_task).returns(nil) | |
26 | end | |
27 | ||
28 | def test_initialize_should_initialize_collections | |
29 | assert_nil @config.rollback_requests | |
30 | assert @config.original_initialize_called | |
31 | assert @config.task_call_frames.empty? | |
32 | end | |
33 | ||
34 | def test_execute_task_should_populate_call_stack | |
35 | task = new_task @config, :testing | |
36 | assert_nothing_raised { @config.execute_task(task) } | |
37 | assert_equal %w(testing), @config.state[:testing][:stack] | |
38 | assert_nil @config.state[:testing][:history] | |
39 | assert @config.task_call_frames.empty? | |
40 | end | |
41 | ||
42 | def test_nested_execute_task_should_add_to_call_stack | |
43 | testing = new_task @config, :testing | |
44 | outer = new_task(@config, :outer) { execute_task(testing) } | |
45 | ||
46 | assert_nothing_raised { @config.execute_task(outer) } | |
47 | assert_equal %w(outer testing), @config.state[:testing][:stack] | |
48 | assert_nil @config.state[:testing][:history] | |
49 | assert @config.task_call_frames.empty? | |
50 | end | |
51 | ||
52 | def test_execute_task_should_execute_in_scope_of_tasks_parent | |
53 | ns = stub("namespace", :tasks => {}, :default_task => nil, :fully_qualified_name => "ns") | |
54 | ns.expects(:instance_eval) | |
55 | testing = new_task ns, :testing | |
56 | @config.execute_task(testing) | |
57 | end | |
58 | ||
59 | def test_transaction_outside_of_task_should_raise_exception | |
60 | assert_raises(ScriptError) { @config.transaction {} } | |
61 | end | |
62 | ||
63 | def test_transaction_without_block_should_raise_argument_error | |
64 | testing = new_task(@config, :testing) { transaction } | |
65 | assert_raises(ArgumentError) { @config.execute_task(testing) } | |
66 | end | |
67 | ||
68 | def test_transaction_should_initialize_transaction_history | |
69 | @config.state[:inspector] = stack_inspector | |
70 | testing = new_task(@config, :testing) { transaction { instance_eval(&state[:inspector]) } } | |
71 | @config.execute_task(testing) | |
72 | assert_equal [], @config.state[:testing][:history] | |
73 | end | |
74 | ||
75 | def test_transaction_from_within_transaction_should_not_start_new_transaction | |
76 | third = new_task(@config, :third, &stack_inspector) | |
77 | second = new_task(@config, :second) { transaction { execute_task(third) } } | |
78 | first = new_task(@config, :first) { transaction { execute_task(second) } } | |
79 | # kind of fragile...not sure how else to check that transaction was only | |
80 | # really run twice...but if the transaction was REALLY run, logger.info | |
81 | # will be called once when it starts, and once when it finishes. | |
82 | @config.logger = mock() | |
83 | @config.logger.stubs(:debug) | |
84 | @config.logger.expects(:info).times(2) | |
85 | @config.execute_task(first) | |
86 | end | |
87 | ||
88 | def test_on_rollback_should_have_no_effect_outside_of_transaction | |
89 | aaa = new_task(@config, :aaa) { on_rollback { state[:rollback] = true }; raise "boom" } | |
90 | assert_raises(RuntimeError) { @config.execute_task(aaa) } | |
91 | assert_nil @config.state[:rollback] | |
92 | end | |
93 | ||
94 | def test_exception_raised_in_transaction_should_call_all_registered_rollback_handlers_in_reverse_order | |
95 | aaa = new_task(@config, :aaa) { on_rollback { (state[:rollback] ||= []) << :aaa } } | |
96 | bbb = new_task(@config, :bbb) { on_rollback { (state[:rollback] ||= []) << :bbb } } | |
97 | ccc = new_task(@config, :ccc) {} | |
98 | ddd = new_task(@config, :ddd) { on_rollback { (state[:rollback] ||= []) << :ddd }; execute_task(bbb); execute_task(ccc) } | |
99 | eee = new_task(@config, :eee) { transaction { execute_task(ddd); execute_task(aaa); raise "boom" } } | |
100 | assert_raises(RuntimeError) do | |
101 | @config.execute_task(eee) | |
102 | end | |
103 | assert_equal [:aaa, :bbb, :ddd], @config.state[:rollback] | |
104 | assert_nil @config.rollback_requests | |
105 | assert @config.task_call_frames.empty? | |
106 | end | |
107 | ||
108 | def test_exception_during_rollback_should_simply_be_logged_and_ignored | |
109 | aaa = new_task(@config, :aaa) { on_rollback { state[:aaa] = true; raise LoadError, "ouch" }; execute_task(bbb) } | |
110 | bbb = new_task(@config, :bbb) { raise MadError, "boom" } | |
111 | ccc = new_task(@config, :ccc) { transaction { execute_task(aaa) } } | |
112 | assert_raises(NameError) do | |
113 | @config.execute_task(ccc) | |
114 | end | |
115 | assert @config.state[:aaa] | |
116 | end | |
117 | ||
118 | def test_on_rollback_called_twice_should_result_in_last_rollback_block_being_effective | |
119 | aaa = new_task(@config, :aaa) do | |
120 | transaction do | |
121 | on_rollback { (state[:rollback] ||= []) << :first } | |
122 | on_rollback { (state[:rollback] ||= []) << :second } | |
123 | raise "boom" | |
124 | end | |
125 | end | |
126 | ||
127 | assert_raises(RuntimeError) do | |
128 | @config.execute_task(aaa) | |
129 | end | |
130 | ||
131 | assert_equal [:second], @config.state[:rollback] | |
132 | end | |
133 | ||
134 | def test_find_and_execute_task_should_raise_error_when_task_cannot_be_found | |
135 | @config.expects(:find_task).with("path:to:task").returns(nil) | |
136 | assert_raises(Capistrano::NoSuchTaskError) { @config.find_and_execute_task("path:to:task") } | |
137 | end | |
138 | ||
139 | def test_find_and_execute_task_should_execute_task_when_task_is_found | |
140 | @config.expects(:find_task).with("path:to:task").returns(:found) | |
141 | @config.expects(:execute_task).with(:found) | |
142 | assert_nothing_raised { @config.find_and_execute_task("path:to:task") } | |
143 | end | |
144 | ||
145 | def test_find_and_execute_task_with_before_option_should_trigger_callback | |
146 | @config.expects(:find_task).with("path:to:task").returns(:found) | |
147 | @config.expects(:trigger).with(:incoming, :found) | |
148 | @config.expects(:execute_task).with(:found) | |
149 | @config.find_and_execute_task("path:to:task", :before => :incoming) | |
150 | end | |
151 | ||
152 | def test_find_and_execute_task_with_after_option_should_trigger_callback | |
153 | @config.expects(:find_task).with("path:to:task").returns(:found) | |
154 | @config.expects(:trigger).with(:outgoing, :found) | |
155 | @config.expects(:execute_task).with(:found) | |
156 | @config.find_and_execute_task("path:to:task", :after => :outgoing) | |
157 | end | |
158 | ||
159 | private | |
160 | ||
161 | def stack_inspector | |
162 | Proc.new do | |
163 | (state[:trail] ||= []) << current_task.fully_qualified_name | |
164 | data = state[current_task.name] = {} | |
165 | data[:stack] = task_call_frames.map { |frame| frame.task.fully_qualified_name } | |
166 | data[:history] = rollback_requests && rollback_requests.map { |frame| frame.task.fully_qualified_name } | |
167 | end | |
168 | end | |
169 | ||
170 | def new_task(namespace, name, options={}, &block) | |
171 | block ||= stack_inspector | |
172 | namespace.tasks[name] = Capistrano::TaskDefinition.new(name, namespace, &block) | |
173 | end | |
174 | end⏎ |
0 | require 'utils' | |
1 | require 'capistrano/configuration/loading' | |
2 | ||
3 | class ConfigurationLoadingTest < Test::Unit::TestCase | |
4 | class MockConfig | |
5 | attr_accessor :ping | |
6 | attr_reader :original_initialize_called | |
7 | ||
8 | def initialize | |
9 | @original_initialize_called = true | |
10 | end | |
11 | ||
12 | def ping!(value) | |
13 | @ping = value | |
14 | end | |
15 | ||
16 | include Capistrano::Configuration::Loading | |
17 | end | |
18 | ||
19 | def setup | |
20 | @config = MockConfig.new | |
21 | end | |
22 | ||
23 | def teardown | |
24 | MockConfig.instance = nil | |
25 | $LOADED_FEATURES.delete_if { |a| a =~ /fixtures\/custom\.rb$/ } | |
26 | end | |
27 | ||
28 | def test_initialize_should_init_collections | |
29 | assert @config.original_initialize_called | |
30 | assert @config.load_paths.include?(".") | |
31 | assert @config.load_paths.detect { |v| v =~ /capistrano\/recipes$/ } | |
32 | end | |
33 | ||
34 | def test_load_with_options_and_block_should_raise_argument_error | |
35 | assert_raises(ArgumentError) do | |
36 | @config.load(:string => "foo") { something } | |
37 | end | |
38 | end | |
39 | ||
40 | def test_load_with_arguments_and_block_should_raise_argument_error | |
41 | assert_raises(ArgumentError) do | |
42 | @config.load("foo") { something } | |
43 | end | |
44 | end | |
45 | ||
46 | def test_load_from_string_should_eval_in_config_scope | |
47 | @config.load :string => "ping! :here" | |
48 | assert_equal :here, @config.ping | |
49 | end | |
50 | ||
51 | def test_load_from_file_shoudld_respect_load_path | |
52 | File.stubs(:file?).returns(false) | |
53 | File.stubs(:file?).with("custom/path/for/file.rb").returns(true) | |
54 | File.stubs(:read).with("custom/path/for/file.rb").returns("ping! :here") | |
55 | ||
56 | @config.load_paths << "custom/path/for" | |
57 | @config.load :file => "file.rb" | |
58 | ||
59 | assert_equal :here, @config.ping | |
60 | end | |
61 | ||
62 | def test_load_from_file_should_respect_load_path_and_appends_rb | |
63 | File.stubs(:file?).returns(false) | |
64 | File.stubs(:file?).with("custom/path/for/file.rb").returns(true) | |
65 | File.stubs(:read).with("custom/path/for/file.rb").returns("ping! :here") | |
66 | ||
67 | @config.load_paths << "custom/path/for" | |
68 | @config.load :file => "file" | |
69 | ||
70 | assert_equal :here, @config.ping | |
71 | end | |
72 | ||
73 | def test_load_from_file_should_raise_load_error_if_file_cannot_be_found | |
74 | File.stubs(:file?).returns(false) | |
75 | assert_raises(LoadError) do | |
76 | @config.load :file => "file" | |
77 | end | |
78 | end | |
79 | ||
80 | def test_load_from_proc_should_eval_proc_in_config_scope | |
81 | @config.load :proc => Proc.new { ping! :here } | |
82 | assert_equal :here, @config.ping | |
83 | end | |
84 | ||
85 | def test_load_with_block_should_treat_block_as_proc_parameter | |
86 | @config.load { ping! :here } | |
87 | assert_equal :here, @config.ping | |
88 | end | |
89 | ||
90 | def test_load_with_unrecognized_option_should_raise_argument_error | |
91 | assert_raises(ArgumentError) do | |
92 | @config.load :url => "http://www.load-this.test" | |
93 | end | |
94 | end | |
95 | ||
96 | def test_load_with_arguments_should_treat_arguments_as_files | |
97 | File.stubs(:file?).returns(false) | |
98 | File.stubs(:file?).with("./first.rb").returns(true) | |
99 | File.stubs(:file?).with("./second.rb").returns(true) | |
100 | File.stubs(:read).with("./first.rb").returns("ping! 'this'") | |
101 | File.stubs(:read).with("./second.rb").returns("ping << 'that'") | |
102 | assert_nothing_raised { @config.load "first", "second" } | |
103 | assert_equal "thisthat", @config.ping | |
104 | end | |
105 | ||
106 | def test_require_from_config_should_load_file_in_config_scope | |
107 | assert_nothing_raised do | |
108 | @config.require "#{File.expand_path(File.dirname(__FILE__))}/../fixtures/custom" | |
109 | end | |
110 | assert_equal :custom, @config.ping | |
111 | end | |
112 | ||
113 | def test_require_without_config_should_raise_load_error | |
114 | assert_raises(LoadError) do | |
115 | require "#{File.dirname(__FILE__)}/../fixtures/custom" | |
116 | end | |
117 | end | |
118 | ||
119 | def test_require_from_config_should_return_false_when_called_a_second_time_with_same_args | |
120 | assert @config.require("#{File.expand_path(File.dirname(__FILE__))}/../fixtures/custom") | |
121 | assert_equal false, @config.require("#{File.expand_path(File.dirname(__FILE__))}/../fixtures/custom") | |
122 | end | |
123 | ||
124 | def test_require_in_multiple_instances_should_load_recipes_in_each_instance | |
125 | config2 = MockConfig.new | |
126 | @config.require "#{File.expand_path(File.dirname(__FILE__))}/../fixtures/custom" | |
127 | config2.require "#{File.expand_path(File.dirname(__FILE__))}/../fixtures/custom" | |
128 | assert_equal :custom, @config.ping | |
129 | assert_equal :custom, config2.ping | |
130 | end | |
131 | end |
0 | require "utils" | |
1 | require 'capistrano/configuration/namespaces' | |
2 | ||
3 | class ConfigurationNamespacesDSLTest < Test::Unit::TestCase | |
4 | class MockConfig | |
5 | attr_reader :original_initialize_called, :options | |
6 | ||
7 | def initialize | |
8 | @original_initialize_called = true | |
9 | @options = {} | |
10 | end | |
11 | ||
12 | include Capistrano::Configuration::Namespaces | |
13 | end | |
14 | ||
15 | def setup | |
16 | @config = MockConfig.new | |
17 | end | |
18 | ||
19 | def test_initialize_should_initialize_collections | |
20 | assert @config.original_initialize_called | |
21 | assert @config.tasks.empty? | |
22 | assert @config.namespaces.empty? | |
23 | end | |
24 | ||
25 | def test_unqualified_task_should_define_task_at_top_namespace | |
26 | assert !@config.tasks.key?(:testing) | |
27 | @config.task(:testing) { puts "something" } | |
28 | assert @config.tasks.key?(:testing) | |
29 | end | |
30 | ||
31 | def test_qualification_should_define_task_within_namespace | |
32 | @config.namespace(:testing) do | |
33 | task(:nested) { puts "nested" } | |
34 | end | |
35 | ||
36 | assert !@config.tasks.key?(:nested) | |
37 | assert @config.namespaces.key?(:testing) | |
38 | assert @config.namespaces[:testing].tasks.key?(:nested) | |
39 | end | |
40 | ||
41 | def test_namespace_within_namespace_should_define_task_within_nested_namespace | |
42 | @config.namespace :outer do | |
43 | namespace :inner do | |
44 | task :nested do | |
45 | puts "nested" | |
46 | end | |
47 | end | |
48 | end | |
49 | ||
50 | assert !@config.tasks.key?(:nested) | |
51 | assert @config.namespaces.key?(:outer) | |
52 | assert @config.namespaces[:outer].namespaces.key?(:inner) | |
53 | assert @config.namespaces[:outer].namespaces[:inner].tasks.key?(:nested) | |
54 | end | |
55 | ||
56 | def test_pending_desc_should_apply_only_to_immediately_subsequent_task | |
57 | @config.desc "A description" | |
58 | @config.task(:testing) { puts "foo" } | |
59 | @config.task(:another) { puts "bar" } | |
60 | assert_equal "A description", @config.tasks[:testing].desc | |
61 | assert_nil @config.tasks[:another].desc | |
62 | end | |
63 | ||
64 | def test_pending_desc_should_apply_only_to_next_task_in_any_namespace | |
65 | @config.desc "A description" | |
66 | @config.namespace(:outer) { task(:testing) { puts "foo" } } | |
67 | assert_equal "A description", @config.namespaces[:outer].tasks[:testing].desc | |
68 | end | |
69 | ||
70 | def test_defining_task_without_block_should_raise_error | |
71 | assert_raises(ArgumentError) do | |
72 | @config.task(:testing) | |
73 | end | |
74 | end | |
75 | ||
76 | def test_defining_task_that_shadows_existing_method_should_raise_error | |
77 | assert_raises(ArgumentError) do | |
78 | @config.task(:sprintf) { puts "foo" } | |
79 | end | |
80 | end | |
81 | ||
82 | def test_defining_task_that_shadows_existing_namespace_should_raise_error | |
83 | @config.namespace(:outer) {} | |
84 | assert_raises(ArgumentError) do | |
85 | @config.task(:outer) { puts "foo" } | |
86 | end | |
87 | end | |
88 | ||
89 | def test_defining_namespace_that_shadows_existing_method_should_raise_error | |
90 | assert_raises(ArgumentError) do | |
91 | @config.namespace(:sprintf) {} | |
92 | end | |
93 | end | |
94 | ||
95 | def test_defining_namespace_that_shadows_existing_task_should_raise_error | |
96 | @config.task(:testing) { puts "foo" } | |
97 | assert_raises(ArgumentError) do | |
98 | @config.namespace(:testing) {} | |
99 | end | |
100 | end | |
101 | ||
102 | def test_defining_task_that_shadows_existing_task_should_not_raise_error | |
103 | @config.task(:original) { puts "foo" } | |
104 | assert_nothing_raised do | |
105 | @config.task(:original) { puts "bar" } | |
106 | end | |
107 | end | |
108 | ||
109 | def test_defining_ask_should_add_task_as_method | |
110 | assert !@config.methods.any? { |m| m.to_sym == :original } | |
111 | @config.task(:original) { puts "foo" } | |
112 | assert @config.methods.any? { |m| m.to_sym == :original } | |
113 | end | |
114 | ||
115 | def test_calling_defined_task_should_delegate_to_execute_task | |
116 | @config.task(:original) { puts "foo" } | |
117 | @config.expects(:execute_task).with(@config.tasks[:original]) | |
118 | @config.original | |
119 | end | |
120 | ||
121 | def test_role_inside_namespace_should_raise_error | |
122 | assert_raises(NotImplementedError) do | |
123 | @config.namespace(:outer) do | |
124 | role :app, "hello" | |
125 | end | |
126 | end | |
127 | end | |
128 | ||
129 | def test_name_for_top_level_should_be_nil | |
130 | assert_nil @config.name | |
131 | end | |
132 | ||
133 | def test_parent_for_top_level_should_be_nil | |
134 | assert_nil @config.parent | |
135 | end | |
136 | ||
137 | def test_fqn_for_top_level_should_be_nil | |
138 | assert_nil @config.fully_qualified_name | |
139 | end | |
140 | ||
141 | def test_fqn_for_namespace_should_be_the_name_of_the_namespace | |
142 | @config.namespace(:outer) {} | |
143 | assert_equal "outer", @config.namespaces[:outer].fully_qualified_name | |
144 | end | |
145 | ||
146 | def test_parent_for_namespace_should_be_the_top_level | |
147 | @config.namespace(:outer) {} | |
148 | assert_equal @config, @config.namespaces[:outer].parent | |
149 | end | |
150 | ||
151 | def test_fqn_for_nested_namespace_should_be_color_delimited | |
152 | @config.namespace(:outer) { namespace(:inner) {} } | |
153 | assert_equal "outer:inner", @config.namespaces[:outer].namespaces[:inner].fully_qualified_name | |
154 | end | |
155 | ||
156 | def test_parent_for_nested_namespace_should_be_the_nesting_namespace | |
157 | @config.namespace(:outer) { namespace(:inner) {} } | |
158 | assert_equal @config.namespaces[:outer], @config.namespaces[:outer].namespaces[:inner].parent | |
159 | end | |
160 | ||
161 | def test_find_task_should_dereference_nested_tasks | |
162 | @config.namespace(:outer) do | |
163 | namespace(:inner) { task(:nested) { puts "nested" } } | |
164 | end | |
165 | ||
166 | task = @config.find_task("outer:inner:nested") | |
167 | assert_not_nil task | |
168 | assert_equal "outer:inner:nested", task.fully_qualified_name | |
169 | end | |
170 | ||
171 | def test_find_task_should_return_nil_if_no_task_matches | |
172 | assert_nil @config.find_task("outer:inner:nested") | |
173 | end | |
174 | ||
175 | def test_find_task_should_return_default_if_deferences_to_namespace_and_namespace_has_default | |
176 | @config.namespace(:outer) do | |
177 | namespace(:inner) { task(:default) { puts "nested" } } | |
178 | end | |
179 | ||
180 | task = @config.find_task("outer:inner") | |
181 | assert_not_nil task | |
182 | assert_equal :default, task.name | |
183 | assert_equal "outer:inner", task.namespace.fully_qualified_name | |
184 | end | |
185 | ||
186 | def test_find_task_should_return_nil_if_deferences_to_namespace_and_namespace_has_no_default | |
187 | @config.namespace(:outer) do | |
188 | namespace(:inner) { task(:nested) { puts "nested" } } | |
189 | end | |
190 | ||
191 | assert_nil @config.find_task("outer:inner") | |
192 | end | |
193 | ||
194 | def test_default_task_should_return_nil_for_top_level | |
195 | @config.task(:default) {} | |
196 | assert_nil @config.default_task | |
197 | end | |
198 | ||
199 | def test_default_task_should_return_nil_for_namespace_without_default | |
200 | @config.namespace(:outer) { task(:nested) { puts "nested" } } | |
201 | assert_nil @config.namespaces[:outer].default_task | |
202 | end | |
203 | ||
204 | def test_default_task_should_return_task_for_namespace_with_default | |
205 | @config.namespace(:outer) { task(:default) { puts "nested" } } | |
206 | task = @config.namespaces[:outer].default_task | |
207 | assert_not_nil task | |
208 | assert_equal :default, task.name | |
209 | end | |
210 | ||
211 | def test_task_list_should_return_only_tasks_immediately_within_namespace | |
212 | @config.task(:first) { puts "here" } | |
213 | @config.namespace(:outer) do | |
214 | task(:second) { puts "here" } | |
215 | namespace(:inner) do | |
216 | task(:third) { puts "here" } | |
217 | end | |
218 | end | |
219 | ||
220 | assert_equal %w(first), @config.task_list.map { |t| t.fully_qualified_name } | |
221 | end | |
222 | ||
223 | def test_task_list_with_all_should_return_all_tasks_under_this_namespace_recursively | |
224 | @config.task(:first) { puts "here" } | |
225 | @config.namespace(:outer) do | |
226 | task(:second) { puts "here" } | |
227 | namespace(:inner) do | |
228 | task(:third) { puts "here" } | |
229 | end | |
230 | end | |
231 | ||
232 | assert_equal %w(first outer:inner:third outer:second), @config.task_list(:all).map { |t| t.fully_qualified_name }.sort | |
233 | end | |
234 | ||
235 | def test_namespace_should_respond_to_its_parents_methods | |
236 | @config.namespace(:outer) {} | |
237 | ns = @config.namespaces[:outer] | |
238 | assert ns.respond_to?(:original_initialize_called) | |
239 | end | |
240 | ||
241 | def test_namespace_should_accept_respond_to_with_include_priv_parameter | |
242 | @config.namespace(:outer) {} | |
243 | ns = @config.namespaces[:outer] | |
244 | assert ns.respond_to?(:original_initialize_called, true) | |
245 | end | |
246 | ||
247 | def test_namespace_should_delegate_unknown_messages_to_its_parent | |
248 | @config.namespace(:outer) {} | |
249 | ns = @config.namespaces[:outer] | |
250 | assert ns.original_initialize_called | |
251 | end | |
252 | ||
253 | def test_namespace_should_not_understand_messages_that_neither_it_nor_its_parent_understands | |
254 | @config.namespace(:outer) {} | |
255 | ns = @config.namespaces[:outer] | |
256 | assert_raises(NoMethodError) { ns.alskdfjlsf } | |
257 | end | |
258 | ||
259 | def test_search_task_should_find_tasks_in_current_namespace | |
260 | @config.namespace(:outer) do | |
261 | namespace(:inner) do | |
262 | task(:third) { puts "here" } | |
263 | end | |
264 | end | |
265 | ||
266 | inner = @config.namespaces[:outer].namespaces[:inner] | |
267 | assert_equal inner.tasks[:third], inner.search_task(:third) | |
268 | end | |
269 | ||
270 | def test_search_task_should_find_tasks_in_parent_namespace | |
271 | @config.task(:first) { puts "here" } | |
272 | @config.namespace(:outer) do | |
273 | task(:second) { puts "here" } | |
274 | namespace(:inner) do | |
275 | task(:third) { puts "here" } | |
276 | end | |
277 | end | |
278 | ||
279 | inner = @config.namespaces[:outer].namespaces[:inner] | |
280 | assert_equal @config.tasks[:first], inner.search_task(:first) | |
281 | end | |
282 | ||
283 | def test_search_task_should_return_nil_if_no_tasks_are_found | |
284 | @config.namespace(:outer) { namespace(:inner) {} } | |
285 | inner = @config.namespaces[:outer].namespaces[:inner] | |
286 | assert_nil inner.search_task(:first) | |
287 | end | |
288 | ||
289 | def test_top_should_return_self_if_self_is_top | |
290 | assert_equal @config, @config.top | |
291 | end | |
292 | ||
293 | def test_top_should_return_parent_if_parent_is_top | |
294 | @config.namespace(:outer) {} | |
295 | assert_equal @config, @config.namespaces[:outer].top | |
296 | end | |
297 | ||
298 | def test_top_should_return_topmost_parent_if_self_is_deeply_nested | |
299 | @config.namespace(:outer) { namespace(:middle) { namespace(:inner) {} } } | |
300 | assert_equal @config, @config.namespaces[:outer].namespaces[:middle].namespaces[:inner].top | |
301 | end | |
302 | ||
303 | def test_find_task_should_return_nil_when_empty_inner_task | |
304 | @config.namespace :outer do | |
305 | namespace :inner do | |
306 | end | |
307 | end | |
308 | assert_nil @config.find_task("outer::inner") | |
309 | end | |
310 | ||
311 | def test_kernel_method_clashing_should_not_affect_method_delegation_to_parent | |
312 | @config.class.class_eval do | |
313 | def some_weird_method() 'config' end | |
314 | end | |
315 | ||
316 | @config.namespace(:clash) {} | |
317 | namespace = @config.namespaces[:clash] | |
318 | assert_equal 'config', namespace.some_weird_method | |
319 | ||
320 | Kernel.module_eval do | |
321 | def some_weird_method() 'kernel' end | |
322 | end | |
323 | ||
324 | @config.namespace(:clash2) {} | |
325 | namespace = @config.namespaces[:clash2] | |
326 | assert_equal 'config', namespace.some_weird_method | |
327 | ||
328 | Kernel.send :remove_method, :some_weird_method | |
329 | @config.class.send :remove_method, :some_weird_method | |
330 | end | |
331 | end⏎ |
0 | require "utils" | |
1 | require 'capistrano/configuration/roles' | |
2 | require 'capistrano/server_definition' | |
3 | ||
4 | class ConfigurationRolesTest < Test::Unit::TestCase | |
5 | class MockConfig | |
6 | attr_reader :original_initialize_called | |
7 | ||
8 | def initialize | |
9 | @original_initialize_called = true | |
10 | end | |
11 | ||
12 | include Capistrano::Configuration::Roles | |
13 | end | |
14 | ||
15 | def setup | |
16 | @config = MockConfig.new | |
17 | end | |
18 | ||
19 | def test_initialize_should_initialize_roles_collection | |
20 | assert @config.original_initialize_called | |
21 | assert @config.roles.empty? | |
22 | end | |
23 | ||
24 | def test_roles_for_host_with_one_role | |
25 | @config.role :app, "app1.capistrano.test" | |
26 | @config.role :not_app, "not-app.capistrano.test" | |
27 | app_server = @config.roles[:app].servers.first | |
28 | assert @config.role_names_for_host(app_server)==[ :app ] | |
29 | end | |
30 | ||
31 | def test_roles_for_host_with_multiple_roles | |
32 | @config.server "www.capistrano.test", :db, :worker | |
33 | db_server = @config.roles[:db].servers.first | |
34 | assert_equal @config.role_names_for_host(db_server).map(&:to_s).sort, [ 'db', 'worker' ] | |
35 | end | |
36 | ||
37 | def test_role_should_allow_empty_list | |
38 | @config.role :app | |
39 | assert @config.roles.keys.include?(:app) | |
40 | assert @config.roles[:app].empty? | |
41 | end | |
42 | ||
43 | def test_role_with_one_argument_should_add_to_roles_collection | |
44 | @config.role :app, "app1.capistrano.test" | |
45 | assert_equal [:app], @config.roles.keys | |
46 | assert_role_equals %w(app1.capistrano.test) | |
47 | end | |
48 | ||
49 | def test_role_block_returning_single_string_is_added_to_roles_collection | |
50 | @config.role :app do | |
51 | 'app1.capistrano.test' | |
52 | end | |
53 | assert_role_equals %w(app1.capistrano.test) | |
54 | end | |
55 | ||
56 | def test_role_with_multiple_arguments_should_add_each_to_roles_collection | |
57 | @config.role :app, "app1.capistrano.test", "app2.capistrano.test" | |
58 | assert_equal [:app], @config.roles.keys | |
59 | assert_role_equals %w(app1.capistrano.test app2.capistrano.test) | |
60 | end | |
61 | ||
62 | def test_role_with_block_and_strings_should_add_both_to_roles_collection | |
63 | @config.role :app, 'app1.capistrano.test' do | |
64 | 'app2.capistrano.test' | |
65 | end | |
66 | assert_role_equals %w(app1.capistrano.test app2.capistrano.test) | |
67 | end | |
68 | ||
69 | def test_role_block_returning_array_should_add_each_to_roles_collection | |
70 | @config.role :app do | |
71 | ['app1.capistrano.test', 'app2.capistrano.test'] | |
72 | end | |
73 | assert_role_equals %w(app1.capistrano.test app2.capistrano.test) | |
74 | end | |
75 | ||
76 | def test_role_with_options_should_apply_options_to_each_argument | |
77 | @config.role :app, "app1.capistrano.test", "app2.capistrano.test", :extra => :value | |
78 | @config.roles[:app].each do |server| | |
79 | assert_equal({:extra => :value}, server.options) | |
80 | end | |
81 | end | |
82 | ||
83 | def test_role_with_options_should_apply_options_to_block_results | |
84 | @config.role :app, :extra => :value do | |
85 | ['app1.capistrano.test', 'app2.capistrano.test'] | |
86 | end | |
87 | @config.roles[:app].each do |server| | |
88 | assert_equal({:extra => :value}, server.options) | |
89 | end | |
90 | end | |
91 | ||
92 | def test_options_should_apply_only_to_this_argument_set | |
93 | @config.role :app, 'app1.capistrano.test', 'app2.capistrano.test' do | |
94 | ['app3.capistrano.test', 'app4.capistrano.test'] | |
95 | end | |
96 | @config.role :app, 'app5.capistrano.test', 'app6.capistrano.test', :extra => :value do | |
97 | ['app7.capistrano.test', 'app8.capistrano.test'] | |
98 | end | |
99 | @config.role :app, 'app9.capistrano.test' | |
100 | ||
101 | option_hosts = ['app5.capistrano.test', 'app6.capistrano.test', 'app7.capistrano.test', 'app8.capistrano.test'] | |
102 | @config.roles[:app].each do |server| | |
103 | if (option_hosts.include? server.host) | |
104 | assert_equal({:extra => :value}, server.options) | |
105 | else | |
106 | assert_not_equal({:extra => :value}, server.options) | |
107 | end | |
108 | end | |
109 | end | |
110 | ||
111 | # Here, the source should be more readable than the method name | |
112 | def test_role_block_returns_options_hash_is_merged_with_role_options_argument | |
113 | @config.role :app, :first => :one, :second => :two do | |
114 | ['app1.capistrano.test', 'app2.capistrano.test', {:second => :please, :third => :three}] | |
115 | end | |
116 | @config.roles[:app].each do |server| | |
117 | assert_equal({:first => :one, :second => :please, :third => :three}, server.options) | |
118 | end | |
119 | end | |
120 | ||
121 | def test_role_block_can_override_role_options_argument | |
122 | @config.role :app, :value => :wrong do | |
123 | Capistrano::ServerDefinition.new('app.capistrano.test') | |
124 | end | |
125 | @config.roles[:app].servers | |
126 | @config.roles[:app].servers.each do |server| | |
127 | assert_not_equal({:value => :wrong}, server.options) | |
128 | end | |
129 | end | |
130 | ||
131 | def test_role_block_can_return_nil | |
132 | @config.role :app do | |
133 | nil | |
134 | end | |
135 | assert_role_equals ([]) | |
136 | end | |
137 | ||
138 | def test_role_block_can_return_empty_array | |
139 | @config.role :app do | |
140 | [] | |
141 | end | |
142 | assert_role_equals ([]) | |
143 | end | |
144 | ||
145 | def test_role_definitions_via_server_should_associate_server_with_roles | |
146 | @config.server "www.capistrano.test", :web, :app | |
147 | assert_equal %w(www.capistrano.test), @config.roles[:app].map { |s| s.host } | |
148 | assert_equal %w(www.capistrano.test), @config.roles[:web].map { |s| s.host } | |
149 | end | |
150 | ||
151 | private | |
152 | ||
153 | def assert_role_equals(list) | |
154 | assert_equal list, @config.roles[:app].map { |s| s.host } | |
155 | end | |
156 | end |
0 | require "utils" | |
1 | require 'capistrano/task_definition' | |
2 | require 'capistrano/configuration/servers' | |
3 | ||
4 | class ConfigurationServersTest < Test::Unit::TestCase | |
5 | class MockConfig | |
6 | attr_reader :roles | |
7 | attr_accessor :preserve_roles | |
8 | ||
9 | def initialize | |
10 | @roles = {} | |
11 | @preserve_roles = false | |
12 | end | |
13 | ||
14 | include Capistrano::Configuration::Servers | |
15 | end | |
16 | ||
17 | def setup | |
18 | @config = MockConfig.new | |
19 | role(@config, :app, "app1", :primary => true) | |
20 | role(@config, :app, "app2", "app3") | |
21 | role(@config, :web, "web1", "web2") | |
22 | role(@config, :report, "app2", :no_deploy => true) | |
23 | role(@config, :file, "file", :no_deploy => true) | |
24 | end | |
25 | ||
26 | def test_task_without_roles_should_apply_to_all_defined_hosts | |
27 | task = new_task(:testing) | |
28 | assert_equal %w(app1 app2 app3 web1 web2 file).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort | |
29 | end | |
30 | ||
31 | def test_task_with_explicit_role_list_should_apply_only_to_those_roles | |
32 | task = new_task(:testing, @config, :roles => %w(app web)) | |
33 | assert_equal %w(app1 app2 app3 web1 web2).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort | |
34 | end | |
35 | ||
36 | def test_task_with_single_role_should_apply_only_to_that_role | |
37 | task = new_task(:testing, @config, :roles => :web) | |
38 | assert_equal %w(web1 web2).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort | |
39 | end | |
40 | ||
41 | # NOTE Rather than throw an error, as it used to, we return an | |
42 | # empty array so that if a task is okay with a missing role it can continue on | |
43 | def test_task_with_unknown_role_should_return_empty_array | |
44 | task = new_task(:testing, @config, :roles => :bogus) | |
45 | assert_equal [], @config.find_servers_for_task(task) | |
46 | end | |
47 | ||
48 | def test_task_with_hosts_option_should_apply_only_to_those_hosts | |
49 | task = new_task(:testing, @config, :hosts => %w(foo bar)) | |
50 | assert_equal %w(foo bar).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort | |
51 | end | |
52 | ||
53 | def test_task_with_single_hosts_option_should_apply_only_to_that_host | |
54 | task = new_task(:testing, @config, :hosts => "foo") | |
55 | assert_equal %w(foo).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort | |
56 | end | |
57 | ||
58 | def test_task_with_roles_as_environment_variable_should_apply_only_to_that_role | |
59 | ENV['ROLES'] = "app,file" | |
60 | task = new_task(:testing) | |
61 | assert_equal %w(app1 app2 app3 file).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort | |
62 | ensure | |
63 | ENV.delete('ROLES') | |
64 | end | |
65 | ||
66 | def test_task_with_roles_as_environment_variable_and_preserve_roles_should_apply_only_to_existant_task_role | |
67 | ENV['ROLES'] = "app,file" | |
68 | @config.preserve_roles = true | |
69 | task = new_task(:testing,@config, :roles => :app) | |
70 | assert_equal %w(app1 app2 app3).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort | |
71 | ensure | |
72 | ENV.delete('ROLES') | |
73 | end | |
74 | ||
75 | def test_task_with_roles_as_environment_variable_and_preserve_roles_should_apply_only_to_existant_task_roles | |
76 | ENV['ROLES'] = "app,file,web" | |
77 | @config.preserve_roles = true | |
78 | task = new_task(:testing,@config, :roles => [ :app,:file ]) | |
79 | assert_equal %w(app1 app2 app3 file).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort | |
80 | ensure | |
81 | ENV.delete('ROLES') | |
82 | end | |
83 | ||
84 | def test_task_with_roles_as_environment_variable_and_preserve_roles_should_not_apply_if_not_exists_those_task_roles | |
85 | ENV['ROLES'] = "file,web" | |
86 | @config.preserve_roles = true | |
87 | task = new_task(:testing,@config, :roles => [ :app ]) | |
88 | assert_equal [], @config.find_servers_for_task(task).map { |s| s.host }.sort | |
89 | ensure | |
90 | ENV.delete('ROLES') | |
91 | end | |
92 | ||
93 | def test_task_with_hosts_as_environment_variable_should_apply_only_to_those_hosts | |
94 | ENV['HOSTS'] = "foo,bar" | |
95 | task = new_task(:testing) | |
96 | assert_equal %w(foo bar).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort | |
97 | ensure | |
98 | ENV.delete('HOSTS') | |
99 | end | |
100 | ||
101 | def test_task_with_hosts_as_environment_variable_should_not_inspect_roles_at_all | |
102 | ENV['HOSTS'] = "foo,bar" | |
103 | task = new_task(:testing, @config, :roles => :bogus) | |
104 | assert_equal %w(foo bar).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort | |
105 | ensure | |
106 | ENV.delete('HOSTS') | |
107 | end | |
108 | ||
109 | def test_task_with_hostfilter_environment_variable_should_apply_only_to_those_hosts | |
110 | ENV['HOSTFILTER'] = "app1,web1" | |
111 | task = new_task(:testing) | |
112 | assert_equal %w(app1 web1).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort | |
113 | ensure | |
114 | ENV.delete('HOSTFILTER') | |
115 | end | |
116 | ||
117 | def test_task_with_hostfilter_environment_variable_should_filter_hosts_option | |
118 | ENV['HOSTFILTER'] = "foo" | |
119 | task = new_task(:testing, @config, :hosts => %w(foo bar)) | |
120 | assert_equal %w(foo).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort | |
121 | ensure | |
122 | ENV.delete('HOSTFILTER') | |
123 | end | |
124 | ||
125 | def test_task_with_hostfilter_environment_variable_and_skip_hostfilter_should_not_filter_hosts_option | |
126 | ENV['HOSTFILTER'] = "foo" | |
127 | task = new_task(:testing, @config, :hosts => %w(foo bar), :skip_hostfilter => true) | |
128 | assert_equal %w(foo bar).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort | |
129 | ensure | |
130 | ENV.delete('HOSTFILTER') | |
131 | end | |
132 | ||
133 | def test_task_with_hostrolefilter_environment_variable_should_apply_only_to_those_hosts | |
134 | ENV['HOSTROLEFILTER'] = "web" | |
135 | task = new_task(:testing) | |
136 | assert_equal %w(web1 web2).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort | |
137 | ensure | |
138 | ENV.delete('HOSTROLEFILTER') | |
139 | end | |
140 | ||
141 | def test_task_with_only_should_apply_only_to_matching_tasks | |
142 | task = new_task(:testing, @config, :roles => :app, :only => { :primary => true }) | |
143 | assert_equal %w(app1), @config.find_servers_for_task(task).map { |s| s.host } | |
144 | end | |
145 | ||
146 | def test_task_with_except_should_apply_only_to_matching_tasks | |
147 | task = new_task(:testing, @config, :except => { :no_deploy => true }) | |
148 | assert_equal %w(app1 app2 app3 web1 web2).sort, @config.find_servers_for_task(task).map { |s| s.host }.sort | |
149 | end | |
150 | ||
151 | def test_options_to_find_servers_for_task_should_override_options_in_task | |
152 | task = new_task(:testing, @config, :roles => :web) | |
153 | assert_equal %w(app1 app2 app3).sort, @config.find_servers_for_task(task, :roles => :app).map { |s| s.host }.sort | |
154 | end | |
155 | ||
156 | def test_find_servers_with_lambda_for_hosts_should_be_evaluated | |
157 | assert_equal %w(foo), @config.find_servers(:hosts => lambda { "foo" }).map { |s| s.host }.sort | |
158 | assert_equal %w(bar foo), @config.find_servers(:hosts => lambda { %w(foo bar) }).map { |s| s.host }.sort | |
159 | end | |
160 | ||
161 | def test_find_servers_with_lambda_for_roles_should_be_evaluated | |
162 | assert_equal %w(app1 app2 app3), @config.find_servers(:roles => lambda { :app }).map { |s| s.host }.sort | |
163 | assert_equal %w(app2 file), @config.find_servers(:roles => lambda { [:report, :file] }).map { |s| s.host }.sort | |
164 | end | |
165 | ||
166 | def test_find_servers_with_hosts_nil_or_empty | |
167 | assert_equal [], @config.find_servers(:hosts => nil) | |
168 | assert_equal [], @config.find_servers(:hosts => []) | |
169 | result = @config.find_servers(:hosts => @config.find_servers(:roles => :report)[0]) | |
170 | assert_equal 1, result.size | |
171 | result = @config.find_servers(:hosts => "app1") | |
172 | assert_equal 1, result.size | |
173 | end | |
174 | ||
175 | def test_find_servers_with_rolees_nil_or_empty | |
176 | assert_equal [], @config.find_servers(:roles => nil) | |
177 | assert_equal [], @config.find_servers(:roles => []) | |
178 | result = @config.find_servers(:roles => :report) | |
179 | assert_equal 1, result.size | |
180 | end | |
181 | ||
182 | end |
0 | require "utils" | |
1 | require 'capistrano/configuration/variables' | |
2 | ||
3 | class ConfigurationVariablesTest < Test::Unit::TestCase | |
4 | class MockConfig | |
5 | attr_reader :original_initialize_called | |
6 | ||
7 | def initialize | |
8 | @original_initialize_called = true | |
9 | end | |
10 | ||
11 | include Capistrano::Configuration::Variables | |
12 | end | |
13 | ||
14 | def setup | |
15 | MockConfig.any_instance.stubs(:logger).returns(stub_everything) | |
16 | @config = MockConfig.new | |
17 | end | |
18 | ||
19 | def test_initialize_should_initialize_variables_hash | |
20 | assert @config.original_initialize_called | |
21 | assert_equal({:ssh_options => {}, :logger => @config.logger}, @config.variables) | |
22 | end | |
23 | ||
24 | def test_set_should_add_variable_to_hash | |
25 | @config.set :sample, :value | |
26 | assert_equal :value, @config.variables[:sample] | |
27 | end | |
28 | ||
29 | def test_set_should_convert_variable_name_to_symbol | |
30 | @config.set "sample", :value | |
31 | assert_equal :value, @config.variables[:sample] | |
32 | end | |
33 | ||
34 | def test_set_should_be_aliased_to_square_brackets | |
35 | @config[:sample] = :value | |
36 | assert_equal :value, @config.variables[:sample] | |
37 | end | |
38 | ||
39 | def test_variables_should_be_accessible_as_read_accessors | |
40 | @config[:sample] = :value | |
41 | assert_equal :value, @config.sample | |
42 | end | |
43 | ||
44 | def test_method_missing_should_raise_error_if_no_variable_matches | |
45 | assert_raises(NoMethodError) do | |
46 | @config.sample | |
47 | end | |
48 | end | |
49 | ||
50 | def test_respond_to_should_look_for_variables | |
51 | assert !@config.respond_to?(:sample) | |
52 | @config[:sample] = :value | |
53 | assert @config.respond_to?(:sample) | |
54 | end | |
55 | ||
56 | def test_respond_to_should_be_true_when_passed_a_string | |
57 | assert !@config.respond_to?('sample') | |
58 | @config[:sample] = :value | |
59 | assert @config.respond_to?('sample') | |
60 | end | |
61 | ||
62 | def test_respond_to_with_include_priv_paramter | |
63 | assert !@config.respond_to?(:sample, true) | |
64 | end | |
65 | ||
66 | def test_set_should_require_value | |
67 | assert_raises(ArgumentError) do | |
68 | @config.set(:sample) | |
69 | end | |
70 | end | |
71 | ||
72 | def test_set_should_allow_value_to_be_omitted_if_block_is_given | |
73 | assert_nothing_raised do | |
74 | @config.set(:sample) { :value } | |
75 | end | |
76 | assert_instance_of Proc, @config.variables[:sample] | |
77 | end | |
78 | ||
79 | def test_set_should_not_allow_multiple_values | |
80 | assert_raises(ArgumentError) do | |
81 | @config.set(:sample, :value, :another) | |
82 | end | |
83 | end | |
84 | ||
85 | def test_set_should_not_allow_both_a_value_and_a_block | |
86 | assert_raises(ArgumentError) do | |
87 | @config.set(:sample, :value) { :block } | |
88 | end | |
89 | end | |
90 | ||
91 | def test_set_should_not_allow_capitalized_variables | |
92 | assert_raises(ArgumentError) do | |
93 | @config.set :Sample, :value | |
94 | end | |
95 | end | |
96 | ||
97 | def test_unset_should_remove_variable_from_hash | |
98 | @config.set :sample, :value | |
99 | assert @config.variables.key?(:sample) | |
100 | @config.unset :sample | |
101 | assert !@config.variables.key?(:sample) | |
102 | end | |
103 | ||
104 | def test_unset_should_clear_memory_of_original_proc | |
105 | @config.set(:sample) { :value } | |
106 | @config.fetch(:sample) | |
107 | @config.unset(:sample) | |
108 | assert_equal false, @config.reset!(:sample) | |
109 | end | |
110 | ||
111 | def test_exists_should_report_existance_of_variable_in_hash | |
112 | assert !@config.exists?(:sample) | |
113 | @config[:sample] = :value | |
114 | assert @config.exists?(:sample) | |
115 | end | |
116 | ||
117 | def test_reset_should_do_nothing_if_variable_does_not_exist | |
118 | assert_equal false, @config.reset!(:sample) | |
119 | assert !@config.variables.key?(:sample) | |
120 | end | |
121 | ||
122 | def test_reset_should_do_nothing_if_variable_is_not_a_proc | |
123 | @config.set(:sample, :value) | |
124 | assert_equal false, @config.reset!(:sample) | |
125 | assert_equal :value, @config.variables[:sample] | |
126 | end | |
127 | ||
128 | def test_reset_should_do_nothing_if_proc_variable_has_not_been_dereferenced | |
129 | @config.set(:sample) { :value } | |
130 | assert_equal false, @config.reset!(:sample) | |
131 | assert_instance_of Proc, @config.variables[:sample] | |
132 | end | |
133 | ||
134 | def test_reset_should_restore_variable_to_original_proc_value | |
135 | @config.set(:sample) { :value } | |
136 | assert_instance_of Proc, @config.variables[:sample] | |
137 | @config.fetch(:sample) | |
138 | assert_instance_of Symbol, @config.variables[:sample] | |
139 | assert @config.reset!(:sample) | |
140 | assert_instance_of Proc, @config.variables[:sample] | |
141 | end | |
142 | ||
143 | def test_fetch_should_return_stored_non_proc_value | |
144 | @config.set(:sample, :value) | |
145 | assert_equal :value, @config.fetch(:sample) | |
146 | end | |
147 | ||
148 | def test_fetch_should_raise_index_error_if_variable_does_not_exist | |
149 | assert_raises(IndexError) do | |
150 | @config.fetch(:sample) | |
151 | end | |
152 | end | |
153 | ||
154 | def test_fetch_should_return_default_if_variable_does_not_exist_and_default_is_given | |
155 | assert_nothing_raised do | |
156 | assert_equal :default_value, @config.fetch(:sample, :default_value) | |
157 | end | |
158 | end | |
159 | ||
160 | def test_fetch_should_invoke_block_if_variable_does_not_exist_and_block_is_given | |
161 | assert_nothing_raised do | |
162 | assert_equal :default_value, @config.fetch(:sample) { :default_value } | |
163 | end | |
164 | end | |
165 | ||
166 | def test_fetch_should_raise_argument_error_if_both_default_and_block_are_given | |
167 | assert_raises(ArgumentError) do | |
168 | @config.fetch(:sample, :default1) { :default2 } | |
169 | end | |
170 | end | |
171 | ||
172 | def test_fetch_should_dereference_proc_values | |
173 | @config.set(:sample) { :value } | |
174 | assert_instance_of Proc, @config.variables[:sample] | |
175 | assert_equal :value, @config.fetch(:sample) | |
176 | assert_instance_of Symbol, @config.variables[:sample] | |
177 | end | |
178 | ||
179 | def test_square_brackets_should_alias_fetch | |
180 | @config.set(:sample, :value) | |
181 | assert_equal :value, @config[:sample] | |
182 | end | |
183 | ||
184 | def test_square_brackets_should_return_nil_for_non_existant_variable | |
185 | assert_nothing_raised do | |
186 | assert_nil @config[:sample] | |
187 | end | |
188 | end | |
189 | end⏎ |
0 | require "utils" | |
1 | require 'capistrano/configuration' | |
2 | ||
3 | # These tests are only for testing the integration of the various components | |
4 | # of the Configuration class. To test specific features, please look at the | |
5 | # tests under test/configuration. | |
6 | ||
7 | class ConfigurationTest < Test::Unit::TestCase | |
8 | def setup | |
9 | @config = Capistrano::Configuration.new | |
10 | end | |
11 | ||
12 | def test_connections_execution_loading_namespaces_roles_and_variables_modules_should_integrate_correctly | |
13 | Capistrano::SSH.expects(:connect).with { |s,c| s.host == "www.capistrano.test" && c == @config }.returns(:session) | |
14 | ||
15 | process_args = Proc.new do |tree, session, opts| | |
16 | tree.fallback.command == "echo 'hello world'" && | |
17 | session == [:session] && | |
18 | opts == { :logger => @config.logger } | |
19 | end | |
20 | ||
21 | Capistrano::Command.expects(:process).with(&process_args) | |
22 | ||
23 | @config.load do | |
24 | role :test, "www.capistrano.test" | |
25 | set :message, "hello world" | |
26 | namespace :testing do | |
27 | task :example, :roles => :test do | |
28 | run "echo '#{message}'" | |
29 | end | |
30 | end | |
31 | end | |
32 | ||
33 | @config.testing.example | |
34 | end | |
35 | ||
36 | def test_tasks_in_nested_namespace_should_be_able_to_call_tasks_in_same_namespace | |
37 | @config.namespace(:outer) do | |
38 | task(:first) { set :called_first, true } | |
39 | namespace(:inner) do | |
40 | task(:first) { set :called_inner_first, true } | |
41 | task(:second) { first } | |
42 | end | |
43 | end | |
44 | ||
45 | @config.outer.inner.second | |
46 | assert !@config[:called_first] | |
47 | assert @config[:called_inner_first] | |
48 | end | |
49 | ||
50 | def test_tasks_in_nested_namespace_should_be_able_to_call_tasks_in_parent_namespace | |
51 | @config.namespace(:outer) do | |
52 | task(:first) { set :called_first, true } | |
53 | namespace(:inner) do | |
54 | task(:second) { first } | |
55 | end | |
56 | end | |
57 | ||
58 | @config.outer.inner.second | |
59 | assert @config[:called_first] | |
60 | end | |
61 | ||
62 | def test_tasks_in_nested_namespace_should_be_able_to_call_shadowed_tasks_in_parent_namespace | |
63 | @config.namespace(:outer) do | |
64 | task(:first) { set :called_first, true } | |
65 | namespace(:inner) do | |
66 | task(:first) { set :called_inner_first, true } | |
67 | task(:second) { parent.first } | |
68 | end | |
69 | end | |
70 | ||
71 | @config.outer.inner.second | |
72 | assert @config[:called_first] | |
73 | assert !@config[:called_inner_first] | |
74 | end | |
75 | ||
76 | end |
0 | require "utils" | |
1 | require 'capistrano/recipes/deploy/local_dependency' | |
2 | ||
3 | class LocalDependencyTest < Test::Unit::TestCase | |
4 | def setup | |
5 | @config = { } | |
6 | @dependency = Capistrano::Deploy::LocalDependency.new(@config) | |
7 | end | |
8 | ||
9 | def test_should_use_standard_error_message | |
10 | setup_for_one_path_entry(false) | |
11 | @dependency.command("cat") | |
12 | assert_equal "`cat' could not be found in the path on the local host", @dependency.message | |
13 | end | |
14 | ||
15 | def test_should_use_alternative_message_if_provided | |
16 | setup_for_one_path_entry(false) | |
17 | @dependency.command("cat").or("Sorry") | |
18 | assert_equal "Sorry", @dependency.message | |
19 | end | |
20 | ||
21 | def test_env_with_no_path_should_never_find_command | |
22 | ENV.expects(:[]).with("PATH").returns(nil) | |
23 | assert !@dependency.command("cat").pass? | |
24 | end | |
25 | ||
26 | def test_env_with_one_path_entry_should_fail_if_command_not_found | |
27 | setup_for_one_path_entry(false) | |
28 | assert !@dependency.command("cat").pass? | |
29 | end | |
30 | ||
31 | def test_env_with_one_path_entry_should_pass_if_command_found | |
32 | setup_for_one_path_entry(true) | |
33 | assert @dependency.command("cat").pass? | |
34 | end | |
35 | ||
36 | def test_env_with_three_path_entries_should_fail_if_command_not_found | |
37 | setup_for_three_path_entries(false) | |
38 | assert !@dependency.command("cat").pass? | |
39 | end | |
40 | ||
41 | def test_env_with_three_path_entries_should_pass_if_command_found | |
42 | setup_for_three_path_entries(true) | |
43 | assert @dependency.command("cat").pass? | |
44 | end | |
45 | ||
46 | def test_env_with_one_path_entry_on_windows_should_pass_if_command_found_with_extension | |
47 | setup_for_one_path_entry_on_windows(true) | |
48 | assert @dependency.command("cat").pass? | |
49 | end | |
50 | ||
51 | private | |
52 | ||
53 | def setup_for_one_path_entry(command_found) | |
54 | Capistrano::Deploy::LocalDependency.expects(:on_windows?).returns(false) | |
55 | ENV.expects(:[]).with("PATH").returns("/bin") | |
56 | File.expects(:executable?).with("/bin/cat").returns(command_found) | |
57 | end | |
58 | ||
59 | def setup_for_three_path_entries(command_found) | |
60 | Capistrano::Deploy::LocalDependency.expects(:on_windows?).returns(false) | |
61 | path = %w(/bin /usr/bin /usr/local/bin).join(File::PATH_SEPARATOR) | |
62 | ENV.expects(:[]).with("PATH").returns(path) | |
63 | File.expects(:executable?).with("/usr/bin/cat").returns(command_found) | |
64 | File.expects(:executable?).at_most(1).with("/bin/cat").returns(false) | |
65 | File.expects(:executable?).at_most(1).with("/usr/local/bin/cat").returns(false) | |
66 | end | |
67 | ||
68 | def setup_for_one_path_entry_on_windows(command_found) | |
69 | Capistrano::Deploy::LocalDependency.expects(:on_windows?).returns(true) | |
70 | ENV.expects(:[]).with("PATH").returns("/cygwin/bin") | |
71 | File.stubs(:executable?).returns(false) | |
72 | first_executable_extension = Capistrano::Deploy::LocalDependency.windows_executable_extensions.first | |
73 | File.expects(:executable?).with("/cygwin/bin/cat#{first_executable_extension}").returns(command_found) | |
74 | end | |
75 | end |
0 | require "utils" | |
1 | require 'capistrano/recipes/deploy/remote_dependency' | |
2 | ||
3 | class RemoteDependencyTest < Test::Unit::TestCase | |
4 | def setup | |
5 | @config = { } | |
6 | @dependency = Capistrano::Deploy::RemoteDependency.new(@config) | |
7 | end | |
8 | ||
9 | def test_should_use_standard_error_message_for_directory | |
10 | setup_for_a_configuration_run("test -d /data", false) | |
11 | @dependency.directory("/data") | |
12 | assert_equal "`/data' is not a directory (host)", @dependency.message | |
13 | end | |
14 | ||
15 | def test_should_use_standard_error_message_for_file | |
16 | setup_for_a_configuration_run("test -f /data/foo.txt", false) | |
17 | @dependency.file("/data/foo.txt") | |
18 | assert_equal "`/data/foo.txt' is not a file (host)", @dependency.message | |
19 | end | |
20 | ||
21 | def test_should_use_standard_error_message_for_writable | |
22 | setup_for_a_configuration_run("test -w /data/foo.txt", false) | |
23 | @dependency.writable("/data/foo.txt") | |
24 | assert_equal "`/data/foo.txt' is not writable (host)", @dependency.message | |
25 | end | |
26 | ||
27 | def test_should_use_standard_error_message_for_command | |
28 | setup_for_a_configuration_run("which cat", false) | |
29 | @dependency.command("cat") | |
30 | assert_equal "`cat' could not be found in the path (host)", @dependency.message | |
31 | end | |
32 | ||
33 | def test_should_use_standard_error_message_for_gem | |
34 | setup_for_a_configuration_gem_run("capistrano", "9.9", false) | |
35 | @dependency.gem("capistrano", 9.9) | |
36 | assert_equal "gem `capistrano' 9.9 could not be found (host)", @dependency.message | |
37 | end | |
38 | ||
39 | def test_should_use_standard_error_message_for_deb | |
40 | setup_for_a_configuration_deb_run("dpkg", "1.15", false) | |
41 | @dependency.deb("dpkg", "1.15") | |
42 | assert_equal "package `dpkg' 1.15 could not be found (host)", @dependency.message | |
43 | end | |
44 | ||
45 | def test_should_use_standard_error_message_for_rpm | |
46 | setup_for_a_configuration_rpm_run("rpm", "4.8", false) | |
47 | @dependency.rpm("rpm", "4.8") | |
48 | assert_equal "package `rpm' 4.8 could not be found (host)", @dependency.message | |
49 | end | |
50 | ||
51 | def test_should_fail_if_directory_not_found | |
52 | setup_for_a_configuration_run("test -d /data", false) | |
53 | assert !@dependency.directory("/data").pass? | |
54 | end | |
55 | ||
56 | def test_should_pass_if_directory_found | |
57 | setup_for_a_configuration_run("test -d /data", true) | |
58 | assert @dependency.directory("/data").pass? | |
59 | end | |
60 | ||
61 | def test_should_fail_if_file_not_found | |
62 | setup_for_a_configuration_run("test -f /data/foo.txt", false) | |
63 | assert !@dependency.file("/data/foo.txt").pass? | |
64 | end | |
65 | ||
66 | def test_should_pass_if_file_found | |
67 | setup_for_a_configuration_run("test -f /data/foo.txt", true) | |
68 | assert @dependency.file("/data/foo.txt").pass? | |
69 | end | |
70 | ||
71 | def test_should_fail_if_writable_not_found | |
72 | setup_for_a_configuration_run("test -w /data/foo.txt", false) | |
73 | assert !@dependency.writable("/data/foo.txt").pass? | |
74 | end | |
75 | ||
76 | def test_should_pass_if_writable_found | |
77 | setup_for_a_configuration_run("test -w /data/foo.txt", true) | |
78 | assert @dependency.writable("/data/foo.txt").pass? | |
79 | end | |
80 | ||
81 | def test_should_fail_if_command_not_found | |
82 | setup_for_a_configuration_run("which cat", false) | |
83 | assert !@dependency.command("cat").pass? | |
84 | end | |
85 | ||
86 | def test_should_pass_if_command_found | |
87 | setup_for_a_configuration_run("which cat", true) | |
88 | assert @dependency.command("cat").pass? | |
89 | end | |
90 | ||
91 | def test_should_fail_if_gem_not_found | |
92 | setup_for_a_configuration_gem_run("capistrano", "9.9", false) | |
93 | assert !@dependency.gem("capistrano", 9.9).pass? | |
94 | end | |
95 | ||
96 | def test_should_pass_if_gem_found | |
97 | setup_for_a_configuration_gem_run("capistrano", "9.9", true) | |
98 | assert @dependency.gem("capistrano", 9.9).pass? | |
99 | end | |
100 | ||
101 | def test_should_pass_if_deb_found | |
102 | setup_for_a_configuration_deb_run("dpkg", "1.15", true) | |
103 | assert @dependency.deb("dpkg", "1.15").pass? | |
104 | end | |
105 | ||
106 | def test_should_fail_if_deb_not_found | |
107 | setup_for_a_configuration_deb_run("dpkg", "1.15", false) | |
108 | assert !@dependency.deb("dpkg", "1.15").pass? | |
109 | end | |
110 | ||
111 | def test_should_use_alternative_message_if_provided | |
112 | setup_for_a_configuration_run("which cat", false) | |
113 | @dependency.command("cat").or("Sorry") | |
114 | assert_equal "Sorry (host)", @dependency.message | |
115 | end | |
116 | ||
117 | private | |
118 | ||
119 | def setup_for_a_configuration_run(command, passing) | |
120 | expectation = @config.expects(:invoke_command).with(command, {}) | |
121 | if passing | |
122 | expectation.returns(true) | |
123 | else | |
124 | error = Capistrano::CommandError.new | |
125 | error.expects(:hosts).returns(["host"]) | |
126 | expectation.raises(error) | |
127 | end | |
128 | end | |
129 | ||
130 | def setup_for_a_configuration_gem_run(name, version, passing) | |
131 | @config.expects(:fetch).with(:gem_command, "gem").returns("gem") | |
132 | find_gem_cmd = "gem specification --version '#{version}' #{name} 2>&1 | awk 'BEGIN { s = 0 } /^name:/ { s = 1; exit }; END { if(s == 0) exit 1 }'" | |
133 | setup_for_a_configuration_run(find_gem_cmd, passing) | |
134 | end | |
135 | ||
136 | def setup_for_a_configuration_deb_run(name, version, passing) | |
137 | find_deb_cmd = "dpkg -s #{name} | grep '^Version: #{version}'" | |
138 | setup_for_a_configuration_run(find_deb_cmd, passing) | |
139 | end | |
140 | ||
141 | def setup_for_a_configuration_rpm_run(name, version, passing) | |
142 | find_rpm_cmd = "rpm -q #{name} | grep '#{version}'" | |
143 | setup_for_a_configuration_run(find_rpm_cmd, passing) | |
144 | end | |
145 | end |
0 | require "utils" | |
1 | require 'capistrano/recipes/deploy/scm/accurev' | |
2 | ||
3 | class AccurevTest < Test::Unit::TestCase | |
4 | include Capistrano::Deploy::SCM | |
5 | ||
6 | def test_internal_revision_to_s | |
7 | assert_equal 'foo/1', Accurev::InternalRevision.new('foo', 1).to_s | |
8 | assert_equal 'foo/highest', Accurev::InternalRevision.new('foo', 'highest').to_s | |
9 | end | |
10 | ||
11 | def test_internal_revision_parse | |
12 | revision = Accurev::InternalRevision.parse('foo') | |
13 | assert_equal 'foo', revision.stream | |
14 | assert_equal 'highest', revision.transaction_id | |
15 | assert_equal 'foo/highest', revision.to_s | |
16 | ||
17 | revision = Accurev::InternalRevision.parse('foo/1') | |
18 | assert_equal 'foo', revision.stream | |
19 | assert_equal '1', revision.transaction_id | |
20 | assert_equal 'foo/1', revision.to_s | |
21 | end | |
22 | end |
0 | require "utils" | |
1 | require 'capistrano/recipes/deploy/scm/base' | |
2 | ||
3 | class DeploySCMBaseTest < Test::Unit::TestCase | |
4 | class TestSCM < Capistrano::Deploy::SCM::Base | |
5 | default_command "floopy" | |
6 | end | |
7 | ||
8 | def setup | |
9 | @config = { } | |
10 | def @config.exists?(name); key?(name); end | |
11 | ||
12 | @source = TestSCM.new(@config) | |
13 | end | |
14 | ||
15 | def test_command_should_default_to_default_command | |
16 | assert_equal "floopy", @source.command | |
17 | @source.local { assert_equal "floopy", @source.command } | |
18 | end | |
19 | ||
20 | def test_command_should_use_scm_command_if_available | |
21 | @config[:scm_command] = "/opt/local/bin/floopy" | |
22 | assert_equal "/opt/local/bin/floopy", @source.command | |
23 | end | |
24 | ||
25 | def test_command_should_use_scm_command_in_local_mode_if_local_scm_command_not_set | |
26 | @config[:scm_command] = "/opt/local/bin/floopy" | |
27 | @source.local { assert_equal "/opt/local/bin/floopy", @source.command } | |
28 | end | |
29 | ||
30 | def test_command_should_use_local_scm_command_in_local_mode_if_local_scm_command_is_set | |
31 | @config[:scm_command] = "/opt/local/bin/floopy" | |
32 | @config[:local_scm_command] = "/usr/local/bin/floopy" | |
33 | assert_equal "/opt/local/bin/floopy", @source.command | |
34 | @source.local { assert_equal "/usr/local/bin/floopy", @source.command } | |
35 | end | |
36 | ||
37 | def test_command_should_use_default_if_scm_command_is_default | |
38 | @config[:scm_command] = :default | |
39 | assert_equal "floopy", @source.command | |
40 | end | |
41 | ||
42 | def test_command_should_use_default_in_local_mode_if_local_scm_command_is_default | |
43 | @config[:scm_command] = "/foo/bar/floopy" | |
44 | @config[:local_scm_command] = :default | |
45 | @source.local { assert_equal "floopy", @source.command } | |
46 | end | |
47 | ||
48 | def test_local_mode_proxy_should_treat_messages_as_being_in_local_mode | |
49 | @config[:scm_command] = "/foo/bar/floopy" | |
50 | @config[:local_scm_command] = :default | |
51 | assert_equal "floopy", @source.local.command | |
52 | assert_equal "/foo/bar/floopy", @source.command | |
53 | end | |
54 | end |
0 | require "utils" | |
1 | require 'capistrano/recipes/deploy/scm/bzr' | |
2 | ||
3 | class DeploySCMBzrTest < Test::Unit::TestCase | |
4 | class TestSCM < Capistrano::Deploy::SCM::Bzr | |
5 | default_command "bzr" | |
6 | end | |
7 | ||
8 | def setup | |
9 | @config = { :repository => "." } | |
10 | ||
11 | def @config.exists?(name); key?(name); end # is this actually needed? | |
12 | ||
13 | @source = TestSCM.new(@config) | |
14 | end | |
15 | ||
16 | # The bzr scm does not support pseudo-ids. The bzr adapter uses symbol :head | |
17 | # to refer to the recently committed revision. | |
18 | def test_head_revision | |
19 | assert_equal(:head, | |
20 | @source.head, | |
21 | "Since bzr doesn't know a real head revision, symbol :head is used instead.") | |
22 | end | |
23 | ||
24 | # The bzr scm does support many different ways to specify a revision. Only | |
25 | # symbol :head triggers the bzr command 'revno'. | |
26 | def test_query_revision | |
27 | assert_equal("bzr revno #{@config[:repository]}", | |
28 | @source.query_revision(:head) { |o| o }, | |
29 | "Query for :head revision should call bzr command 'revno' in repository directory.") | |
30 | ||
31 | # Many valid revision specifications, some invalid on the last line | |
32 | revision_samples = [ 5, -7, '2', '-4', | |
33 | 'revid:revid:aaaa@bbbb-123456789', | |
34 | 'submit:', | |
35 | 'ancestor:/path/to/branch', | |
36 | 'date:yesterday', | |
37 | 'branch:/path/to/branch', | |
38 | 'tag:trunk', | |
39 | 'revno:3:/path/to/branch', | |
40 | 'before:revid:aaaa@bbbb-1234567890', | |
41 | 'last:3', | |
42 | nil, {}, [], true, false, 1.34, ] | |
43 | ||
44 | revision_samples.each do |revivsion_spec| | |
45 | assert_equal(revivsion_spec, | |
46 | @source.query_revision(revivsion_spec), | |
47 | "Any revision specification other than symbol :head should simply by returned.") | |
48 | end | |
49 | end | |
50 | end |
0 | require "utils" | |
1 | require 'capistrano/recipes/deploy/scm/darcs' | |
2 | ||
3 | class DeploySCMDarcsTest < Test::Unit::TestCase | |
4 | class TestSCM < Capistrano::Deploy::SCM::Darcs | |
5 | default_command "darcs" | |
6 | end | |
7 | def setup | |
8 | @config = { :repository => "." } | |
9 | # def @config.exists?(name); key?(name); end | |
10 | ||
11 | @source = TestSCM.new(@config) | |
12 | end | |
13 | ||
14 | # We should be able to pick a specific hash. | |
15 | def test_checkout_hash | |
16 | hsh = "*version_hash*" | |
17 | assert_match(%r{--to-match=.hash #{Regexp.quote(hsh)}}, | |
18 | @source.checkout(hsh, "*foo_location*"), | |
19 | "Specifying a revision hash got the --to-match option wrong.") | |
20 | end | |
21 | ||
22 | # Picking the head revision should leave out the hash, because head is the | |
23 | # default and we don't have a HEAD pseudotag | |
24 | def test_checkout_head | |
25 | hsh = @source.head | |
26 | assert_no_match(%r{--to-match}, @source.checkout(hsh, "*foo_location*"), | |
27 | "Selecting the head revision incorrectly produced a --to-match option.") | |
28 | end | |
29 | ||
30 | # Leaving the revision as nil shouldn't break anything. | |
31 | def test_checkout_nil | |
32 | assert_no_match(%r{--to-match}, @source.checkout(nil, "*foo_location*"), | |
33 | "Leaving the revision as nil incorrectly produced a --to-match option.") | |
34 | end | |
35 | end | |
36 |
0 | require "utils" | |
1 | require 'capistrano/recipes/deploy/scm/git' | |
2 | ||
3 | class DeploySCMGitTest < Test::Unit::TestCase | |
4 | class TestSCM < Capistrano::Deploy::SCM::Git | |
5 | default_command "git" | |
6 | end | |
7 | ||
8 | def setup | |
9 | @config = { :repository => "." } | |
10 | def @config.exists?(name); key?(name); end | |
11 | ||
12 | @source = TestSCM.new(@config) | |
13 | end | |
14 | ||
15 | def test_head | |
16 | assert_equal "HEAD", @source.head | |
17 | ||
18 | # With :branch | |
19 | @config[:branch] = "master" | |
20 | assert_equal "master", @source.head | |
21 | end | |
22 | ||
23 | def test_origin | |
24 | assert_equal "origin", @source.origin | |
25 | @config[:remote] = "username" | |
26 | assert_equal "username", @source.origin | |
27 | end | |
28 | ||
29 | def test_checkout | |
30 | @config[:repository] = "git@somehost.com:project.git" | |
31 | dest = "/var/www" | |
32 | rev = 'c2d9e79' | |
33 | assert_equal "git clone -q git@somehost.com:project.git /var/www && cd /var/www && git checkout -q -b deploy #{rev}", @source.checkout(rev, dest) | |
34 | ||
35 | # With :scm_command | |
36 | git = "/opt/local/bin/git" | |
37 | @config[:scm_command] = git | |
38 | assert_equal "#{git} clone -q git@somehost.com:project.git /var/www && cd /var/www && #{git} checkout -q -b deploy #{rev}", @source.checkout(rev, dest).gsub(/\s+/, ' ') | |
39 | ||
40 | # with submodules | |
41 | @config[:git_enable_submodules] = true | |
42 | assert_equal "#{git} clone -q git@somehost.com:project.git /var/www && cd /var/www && #{git} checkout -q -b deploy #{rev} && #{git} submodule -q init && #{git} submodule -q sync && export GIT_RECURSIVE=$([ ! \"`#{git} --version`\" \\< \"git version 1.6.5\" ] && echo --recursive) && #{git} submodule -q update --init $GIT_RECURSIVE", @source.checkout(rev, dest).gsub(/\s+/, ' ') | |
43 | end | |
44 | ||
45 | def test_checkout_submodules_without_recursive | |
46 | @config[:repository] = "git@somehost.com:project.git" | |
47 | dest = "/var/www" | |
48 | rev = 'c2d9e79' | |
49 | @config[:git_enable_submodules] = true | |
50 | @config[:git_submodules_recursive] = false | |
51 | assert_equal "git clone -q git@somehost.com:project.git /var/www && cd /var/www && git checkout -q -b deploy #{rev} && git submodule -q init && git submodule -q sync && git submodule -q update --init", @source.checkout(rev, dest).gsub(/\s+/, ' ') | |
52 | end | |
53 | ||
54 | def test_checkout_with_verbose_should_not_use_q_switch | |
55 | @config[:repository] = "git@somehost.com:project.git" | |
56 | @config[:scm_verbose] = true | |
57 | dest = "/var/www" | |
58 | rev = 'c2d9e79' | |
59 | assert_equal "git clone git@somehost.com:project.git /var/www && cd /var/www && git checkout -b deploy #{rev}", @source.checkout(rev, dest) | |
60 | end | |
61 | ||
62 | def test_checkout_with_verbose_off_should_use_q_switch | |
63 | @config[:repository] = "git@somehost.com:project.git" | |
64 | @config[:scm_verbose] = false | |
65 | dest = "/var/www" | |
66 | rev = 'c2d9e79' | |
67 | assert_equal "git clone -q git@somehost.com:project.git /var/www && cd /var/www && git checkout -q -b deploy #{rev}", @source.checkout(rev, dest) | |
68 | end | |
69 | ||
70 | def test_diff | |
71 | assert_equal "git diff master", @source.diff('master') | |
72 | assert_equal "git diff master..branch", @source.diff('master', 'branch') | |
73 | end | |
74 | ||
75 | def test_log | |
76 | assert_equal "git log master..", @source.log('master') | |
77 | assert_equal "git log master..branch", @source.log('master', 'branch') | |
78 | end | |
79 | ||
80 | def test_query_revision_from_remote | |
81 | revision = @source.query_revision('HEAD') do |o| | |
82 | assert_equal "git ls-remote . HEAD", o | |
83 | "d11006102c07c94e5d54dd0ee63dca825c93ed61\tHEAD" | |
84 | end | |
85 | assert_equal "d11006102c07c94e5d54dd0ee63dca825c93ed61", revision | |
86 | end | |
87 | ||
88 | def test_query_revision_falls_back_to_local | |
89 | revision = @source.query_revision('d11006') do |o| | |
90 | return nil if o == "git ls-remote . d11006" | |
91 | assert_equal "git rev-parse --revs-only d11006", o | |
92 | "d11006102c07c94e5d54dd0ee63dca825c93ed61" | |
93 | end | |
94 | assert_equal "d11006102c07c94e5d54dd0ee63dca825c93ed61", revision | |
95 | end | |
96 | ||
97 | def test_query_revision_has_whitespace | |
98 | revision = @source.query_revision('HEAD') do |o| | |
99 | assert_equal "git ls-remote . HEAD", o | |
100 | "d11006102c07c94e5d54dd0ee63dca825c93ed61\tHEAD\r" | |
101 | end | |
102 | assert_equal "d11006102c07c94e5d54dd0ee63dca825c93ed61", revision | |
103 | end | |
104 | ||
105 | def test_query_revision_deprecation_error | |
106 | assert_raise(ArgumentError) do | |
107 | revision = @source.query_revision('origin/release') {} | |
108 | end | |
109 | end | |
110 | ||
111 | def test_command_should_be_backwards_compatible | |
112 | # 1.x version of this module used ":git", not ":scm_command" | |
113 | @config[:git] = "/srv/bin/git" | |
114 | assert_equal "/srv/bin/git", @source.command | |
115 | end | |
116 | ||
117 | def test_sync | |
118 | dest = "/var/www" | |
119 | rev = 'c2d9e79' | |
120 | assert_equal "cd #{dest} && git fetch -q origin && git fetch --tags -q origin && git reset -q --hard #{rev} && git clean -q -d -x -f", @source.sync(rev, dest) | |
121 | ||
122 | # With :scm_command | |
123 | git = "/opt/local/bin/git" | |
124 | @config[:scm_command] = git | |
125 | assert_equal "cd #{dest} && #{git} fetch -q origin && #{git} fetch --tags -q origin && #{git} reset -q --hard #{rev} && #{git} clean -q -d -x -f", @source.sync(rev, dest) | |
126 | ||
127 | # with submodules | |
128 | @config[:git_enable_submodules] = true | |
129 | assert_equal "cd #{dest} && #{git} fetch -q origin && #{git} fetch --tags -q origin && #{git} reset -q --hard #{rev} && #{git} submodule -q init && for mod in `#{git} submodule status | awk '{ print $2 }'`; do #{git} config -f .git/config submodule.${mod}.url `#{git} config -f .gitmodules --get submodule.${mod}.url` && echo Synced $mod; done && #{git} submodule -q sync && export GIT_RECURSIVE=$([ ! \"`#{git} --version`\" \\< \"git version 1.6.5\" ] && echo --recursive) && #{git} submodule -q update --init $GIT_RECURSIVE && #{git} clean -q -d -x -f", @source.sync(rev, dest) | |
130 | end | |
131 | ||
132 | def test_sync_with_remote | |
133 | dest = "/var/www" | |
134 | rev = 'c2d9e79' | |
135 | remote = "username" | |
136 | repository = "git@somehost.com:project.git" | |
137 | ||
138 | @config[:repository] = repository | |
139 | @config[:remote] = remote | |
140 | ||
141 | assert_equal "cd #{dest} && git config remote.#{remote}.url #{repository} && git config remote.#{remote}.fetch +refs/heads/*:refs/remotes/#{remote}/* && git fetch -q #{remote} && git fetch --tags -q username && git reset -q --hard #{rev} && git clean -q -d -x -f", @source.sync(rev, dest) | |
142 | end | |
143 | ||
144 | def test_shallow_clone | |
145 | @config[:repository] = "git@somehost.com:project.git" | |
146 | @config[:git_shallow_clone] = 1 | |
147 | dest = "/var/www" | |
148 | rev = 'c2d9e79' | |
149 | assert_equal "git clone -q --depth 1 git@somehost.com:project.git /var/www && cd /var/www && git checkout -q -b deploy #{rev}", @source.checkout(rev, dest) | |
150 | end | |
151 | ||
152 | def test_remote_clone | |
153 | @config[:repository] = "git@somehost.com:project.git" | |
154 | @config[:remote] = "username" | |
155 | dest = "/var/www" | |
156 | rev = 'c2d9e79' | |
157 | assert_equal "git clone -q -o username git@somehost.com:project.git /var/www && cd /var/www && git checkout -q -b deploy #{rev}", @source.checkout(rev, dest) | |
158 | end | |
159 | ||
160 | def test_remote_clone_with_submodules | |
161 | @config[:repository] = "git@somehost.com:project.git" | |
162 | @config[:remote] = "username" | |
163 | @config[:git_enable_submodules] = true | |
164 | dest = "/var/www" | |
165 | rev = 'c2d9e79' | |
166 | assert_equal "git clone -q -o username git@somehost.com:project.git /var/www && cd /var/www && git checkout -q -b deploy #{rev} && git submodule -q init && git submodule -q sync && export GIT_RECURSIVE=$([ ! \"`git --version`\" \\< \"git version 1.6.5\" ] && echo --recursive) && git submodule -q update --init $GIT_RECURSIVE", @source.checkout(rev, dest) | |
167 | end | |
168 | ||
169 | # Tests from base_test.rb, makin' sure we didn't break anything up there! | |
170 | def test_command_should_default_to_default_command | |
171 | assert_equal "git", @source.command | |
172 | @source.local { assert_equal "git", @source.command } | |
173 | end | |
174 | ||
175 | def test_command_should_use_scm_command_if_available | |
176 | @config[:scm_command] = "/opt/local/bin/git" | |
177 | assert_equal "/opt/local/bin/git", @source.command | |
178 | end | |
179 | ||
180 | def test_command_should_use_scm_command_in_local_mode_if_local_scm_command_not_set | |
181 | @config[:scm_command] = "/opt/local/bin/git" | |
182 | @source.local { assert_equal "/opt/local/bin/git", @source.command } | |
183 | end | |
184 | ||
185 | def test_command_should_use_local_scm_command_in_local_mode_if_local_scm_command_is_set | |
186 | @config[:scm_command] = "/opt/local/bin/git" | |
187 | @config[:local_scm_command] = "/usr/local/bin/git" | |
188 | assert_equal "/opt/local/bin/git", @source.command | |
189 | @source.local { assert_equal "/usr/local/bin/git", @source.command } | |
190 | end | |
191 | ||
192 | def test_command_should_use_default_if_scm_command_is_default | |
193 | @config[:scm_command] = :default | |
194 | assert_equal "git", @source.command | |
195 | end | |
196 | ||
197 | def test_command_should_use_default_in_local_mode_if_local_scm_command_is_default | |
198 | @config[:scm_command] = "/foo/bar/git" | |
199 | @config[:local_scm_command] = :default | |
200 | @source.local { assert_equal "git", @source.command } | |
201 | end | |
202 | ||
203 | def test_local_mode_proxy_should_treat_messages_as_being_in_local_mode | |
204 | @config[:scm_command] = "/foo/bar/git" | |
205 | @config[:local_scm_command] = :default | |
206 | assert_equal "git", @source.local.command | |
207 | assert_equal "/foo/bar/git", @source.command | |
208 | end | |
209 | end | |
210 |
0 | require "utils" | |
1 | require 'capistrano/recipes/deploy/scm/mercurial' | |
2 | ||
3 | class DeploySCMMercurialTest < Test::Unit::TestCase | |
4 | class TestSCM < Capistrano::Deploy::SCM::Mercurial | |
5 | default_command "hg" | |
6 | end | |
7 | ||
8 | def setup | |
9 | @config = { } | |
10 | def @config.exists?(name); key?(name); end | |
11 | ||
12 | @source = TestSCM.new(@config) | |
13 | end | |
14 | ||
15 | def test_head | |
16 | assert_equal "tip", @source.head | |
17 | end | |
18 | ||
19 | def test_different_head | |
20 | @config[:branch] = "staging" | |
21 | assert_equal "staging", @source.head | |
22 | end | |
23 | ||
24 | def test_checkout | |
25 | @config[:repository] = "http://example.com/project-hg" | |
26 | dest = "/var/www" | |
27 | assert_equal "hg clone --noupdate http://example.com/project-hg /var/www && hg update --repository /var/www --clean 8a8e00b8f11b", @source.checkout('8a8e00b8f11b', dest) | |
28 | end | |
29 | ||
30 | def test_diff | |
31 | assert_equal "hg diff --rev tip", @source.diff('tip') | |
32 | assert_equal "hg diff --rev 1 --rev 2", @source.diff('1', '2') | |
33 | end | |
34 | ||
35 | def test_log | |
36 | assert_equal "hg log --rev 8a8e00b8f11b", @source.log('8a8e00b8f11b') | |
37 | assert_equal "hg log --rev 0:3", @source.log('0', '3') | |
38 | end | |
39 | ||
40 | def test_query_revision | |
41 | assert_equal "hg log -r 8a8e00b8f11b --template \"{node|short}\"", @source.query_revision('8a8e00b8f11b') { |o| o } | |
42 | end | |
43 | ||
44 | def test_username_should_be_backwards_compatible | |
45 | # older versions of this module required :scm_user var instead | |
46 | # of the currently preferred :scm_username | |
47 | require 'capistrano/logger' | |
48 | @config[:scm_user] = "fred" | |
49 | text = "user:" | |
50 | assert_equal "fred\n", @source.handle_data(mock_state, :test_stream, text) | |
51 | # :scm_username takes priority | |
52 | @config[:scm_username] = "wilma" | |
53 | assert_equal "wilma\n", @source.handle_data(mock_state, :test_stream, text) | |
54 | end | |
55 | ||
56 | def test_sync | |
57 | dest = "/var/www" | |
58 | assert_equal "hg pull --repository /var/www && hg update --repository /var/www --clean 8a8e00b8f11b", @source.sync('8a8e00b8f11b', dest) | |
59 | ||
60 | # With :scm_command | |
61 | @config[:scm_command] = "/opt/local/bin/hg" | |
62 | assert_equal "/opt/local/bin/hg pull --repository /var/www && /opt/local/bin/hg update --repository /var/www --clean 8a8e00b8f11b", @source.sync('8a8e00b8f11b', dest) | |
63 | end | |
64 | ||
65 | def test_export | |
66 | dest = "/var/www" | |
67 | assert_raise(NotImplementedError) { @source.export('8a8e00b8f11b', dest) } | |
68 | end | |
69 | ||
70 | def test_sends_password_if_set | |
71 | require 'capistrano/logger' | |
72 | text = "password:" | |
73 | @config[:scm_password] = "opensesame" | |
74 | assert_equal "opensesame\n", @source.handle_data(mock_state, :test_stream, text) | |
75 | end | |
76 | ||
77 | def test_prompts_for_password_if_preferred | |
78 | require 'capistrano/logger' | |
79 | require 'capistrano/cli' | |
80 | Capistrano::CLI.stubs(:password_prompt).with("hg password: ").returns("opensesame") | |
81 | @config[:scm_prefer_prompt] = true | |
82 | text = "password:" | |
83 | assert_equal "opensesame\n", @source.handle_data(mock_state, :test_stream, text) | |
84 | end | |
85 | ||
86 | ||
87 | # Tests from base_test.rb, makin' sure we didn't break anything up there! | |
88 | def test_command_should_default_to_default_command | |
89 | assert_equal "hg", @source.command | |
90 | @source.local { assert_equal "hg", @source.command } | |
91 | end | |
92 | ||
93 | def test_command_should_use_scm_command_if_available | |
94 | @config[:scm_command] = "/opt/local/bin/hg" | |
95 | assert_equal "/opt/local/bin/hg", @source.command | |
96 | end | |
97 | ||
98 | def test_command_should_use_scm_command_in_local_mode_if_local_scm_command_not_set | |
99 | @config[:scm_command] = "/opt/local/bin/hg" | |
100 | @source.local { assert_equal "/opt/local/bin/hg", @source.command } | |
101 | end | |
102 | ||
103 | def test_command_should_use_local_scm_command_in_local_mode_if_local_scm_command_is_set | |
104 | @config[:scm_command] = "/opt/local/bin/hg" | |
105 | @config[:local_scm_command] = "/usr/local/bin/hg" | |
106 | assert_equal "/opt/local/bin/hg", @source.command | |
107 | @source.local { assert_equal "/usr/local/bin/hg", @source.command } | |
108 | end | |
109 | ||
110 | def test_command_should_use_default_if_scm_command_is_default | |
111 | @config[:scm_command] = :default | |
112 | assert_equal "hg", @source.command | |
113 | end | |
114 | ||
115 | def test_command_should_use_default_in_local_mode_if_local_scm_command_is_default | |
116 | @config[:scm_command] = "/foo/bar/hg" | |
117 | @config[:local_scm_command] = :default | |
118 | @source.local { assert_equal "hg", @source.command } | |
119 | end | |
120 | ||
121 | def test_local_mode_proxy_should_treat_messages_as_being_in_local_mode | |
122 | @config[:scm_command] = "/foo/bar/hg" | |
123 | @config[:local_scm_command] = :default | |
124 | assert_equal "hg", @source.local.command | |
125 | assert_equal "/foo/bar/hg", @source.command | |
126 | end | |
127 | ||
128 | private | |
129 | ||
130 | def mock_state | |
131 | { :channel => { :host => "abc" } } | |
132 | end | |
133 | end |
0 | require 'utils' | |
1 | require 'capistrano/recipes/deploy/scm/none' | |
2 | ||
3 | class DeploySCMNoneTest < Test::Unit::TestCase | |
4 | class TestSCM < Capistrano::Deploy::SCM::None | |
5 | default_command 'none' | |
6 | end | |
7 | ||
8 | def setup | |
9 | @config = {} | |
10 | def @config.exists?(name); key?(name); end | |
11 | @source = TestSCM.new(@config) | |
12 | end | |
13 | ||
14 | def test_the_truth | |
15 | assert true | |
16 | end | |
17 | ||
18 | def test_checkout_on_linux | |
19 | Capistrano::Deploy::LocalDependency.stubs(:on_windows?).returns(false) | |
20 | @config[:repository] = '.' | |
21 | rev = '' | |
22 | dest = '/var/www' | |
23 | assert_equal "cp -R . /var/www", @source.checkout(rev, dest) | |
24 | end | |
25 | ||
26 | def test_checkout_on_windows | |
27 | Capistrano::Deploy::LocalDependency.stubs(:on_windows?).returns(true) | |
28 | @config[:repository] = '.' | |
29 | rev = '' | |
30 | dest = 'c:/Documents and settings/admin/tmp' | |
31 | assert_equal "xcopy . \"c:/Documents and settings/admin/tmp\" /S/I/Y/Q/E", @source.checkout(rev, dest) | |
32 | end | |
33 | ||
34 | end |
0 | require "utils" | |
1 | require 'capistrano/recipes/deploy/scm/perforce' | |
2 | ||
3 | class DeploySCMPerforceTest < Test::Unit::TestCase | |
4 | class TestSCM < Capistrano::Deploy::SCM::Perforce | |
5 | default_command "perforce" | |
6 | end | |
7 | def setup | |
8 | @config = { :repository => "." } | |
9 | @source = TestSCM.new(@config) | |
10 | end | |
11 | ||
12 | def test_p4_label | |
13 | @config[:p4_label] = "some_p4_label" | |
14 | assert_equal "@some_p4_label", @source.send(:rev_no, 'foo') | |
15 | end | |
16 | ||
17 | def test_p4_label_with_symbol | |
18 | @config[:p4_label] = "@some_p4_label" | |
19 | assert_equal "@some_p4_label", @source.send(:rev_no, 'foo') | |
20 | end | |
21 | ||
22 | end⏎ |
0 | require "utils" | |
1 | require 'capistrano/recipes/deploy/scm/subversion' | |
2 | ||
3 | class DeploySCMSubversionTest < Test::Unit::TestCase | |
4 | class TestSCM < Capistrano::Deploy::SCM::Subversion | |
5 | default_command "svn" | |
6 | end | |
7 | ||
8 | def setup | |
9 | @config = { :repository => "." } | |
10 | def @config.exists?(name); key?(name); end | |
11 | ||
12 | @source = TestSCM.new(@config) | |
13 | end | |
14 | ||
15 | def test_query_revision | |
16 | revision = @source.query_revision('HEAD') do |o| | |
17 | assert_equal "svn info . -rHEAD", o | |
18 | %Q{Path: rails_2_3 | |
19 | URL: svn+ssh://example.com/var/repos/project/branches/rails_2_3 | |
20 | Repository Root: svn+ssh://example.com/var/repos | |
21 | Repository UUID: 2d86388d-c40f-0410-ad6a-a69da6a65d20 | |
22 | Revision: 2095 | |
23 | Node Kind: directory | |
24 | Last Changed Author: sw | |
25 | Last Changed Rev: 2064 | |
26 | Last Changed Date: 2009-03-11 11:04:25 -0700 (Wed, 11 Mar 2009) | |
27 | } | |
28 | end | |
29 | assert_equal 2095, revision | |
30 | end | |
31 | ||
32 | def test_sync | |
33 | @config[:repository] = "http://svn.github.com/capistrano/capistrano.git" | |
34 | rev = '602' | |
35 | dest = "/var/www" | |
36 | assert_equal "svn switch -q -r602 http://svn.github.com/capistrano/capistrano.git /var/www", @source.sync(rev, dest) | |
37 | end | |
38 | ||
39 | end |
0 | require "utils" | |
1 | require 'capistrano/logger' | |
2 | require 'capistrano/recipes/deploy/strategy/copy' | |
3 | require 'stringio' | |
4 | ||
5 | class DeployStrategyCopyTest < Test::Unit::TestCase | |
6 | def setup | |
7 | @config = { :application => "captest", | |
8 | :logger => Capistrano::Logger.new(:output => StringIO.new), | |
9 | :releases_path => "/u/apps/test/releases", | |
10 | :release_path => "/u/apps/test/releases/1234567890", | |
11 | :real_revision => "154" } | |
12 | @source = mock("source") | |
13 | @config.stubs(:source).returns(@source) | |
14 | @strategy = Capistrano::Deploy::Strategy::Copy.new(@config) | |
15 | end | |
16 | ||
17 | def test_deploy_with_defaults_should_use_remote_gtar | |
18 | @config[:copy_remote_tar] = 'gtar' | |
19 | ||
20 | Dir.expects(:tmpdir).returns("/temp/dir") | |
21 | @source.expects(:checkout).with("154", "/temp/dir/1234567890").returns(:local_checkout) | |
22 | @strategy.expects(:system).with(:local_checkout) | |
23 | ||
24 | Dir.expects(:chdir).with("/temp/dir").yields | |
25 | @strategy.expects(:system).with("tar czf 1234567890.tar.gz 1234567890") | |
26 | @strategy.expects(:upload).with("/temp/dir/1234567890.tar.gz", "/tmp/1234567890.tar.gz") | |
27 | @strategy.expects(:run).with("cd /u/apps/test/releases && gtar xzf /tmp/1234567890.tar.gz && rm /tmp/1234567890.tar.gz") | |
28 | ||
29 | mock_file = mock("file") | |
30 | mock_file.expects(:puts).with("154") | |
31 | File.expects(:open).with("/temp/dir/1234567890/REVISION", "w").yields(mock_file) | |
32 | ||
33 | FileUtils.expects(:rm).with("/temp/dir/1234567890.tar.gz") | |
34 | FileUtils.expects(:rm_rf).with("/temp/dir/1234567890") | |
35 | ||
36 | @strategy.deploy! | |
37 | end | |
38 | ||
39 | def test_deploy_with_defaults_should_use_local_gtar | |
40 | @config[:copy_local_tar] = 'gtar' | |
41 | ||
42 | Dir.expects(:tmpdir).returns("/temp/dir") | |
43 | @source.expects(:checkout).with("154", "/temp/dir/1234567890").returns(:local_checkout) | |
44 | @strategy.expects(:system).with(:local_checkout) | |
45 | ||
46 | Dir.expects(:chdir).with("/temp/dir").yields | |
47 | @strategy.expects(:system).with("gtar czf 1234567890.tar.gz 1234567890") | |
48 | @strategy.expects(:upload).with("/temp/dir/1234567890.tar.gz", "/tmp/1234567890.tar.gz") | |
49 | @strategy.expects(:run).with("cd /u/apps/test/releases && tar xzf /tmp/1234567890.tar.gz && rm /tmp/1234567890.tar.gz") | |
50 | ||
51 | mock_file = mock("file") | |
52 | mock_file.expects(:puts).with("154") | |
53 | File.expects(:open).with("/temp/dir/1234567890/REVISION", "w").yields(mock_file) | |
54 | ||
55 | FileUtils.expects(:rm).with("/temp/dir/1234567890.tar.gz") | |
56 | FileUtils.expects(:rm_rf).with("/temp/dir/1234567890") | |
57 | ||
58 | @strategy.deploy! | |
59 | end | |
60 | ||
61 | def test_deploy_with_defaults_should_use_tar_gz_and_checkout | |
62 | Dir.expects(:tmpdir).returns("/temp/dir") | |
63 | @source.expects(:checkout).with("154", "/temp/dir/1234567890").returns(:local_checkout) | |
64 | @strategy.expects(:system).with(:local_checkout) | |
65 | ||
66 | prepare_standard_compress_and_copy! | |
67 | @strategy.deploy! | |
68 | end | |
69 | ||
70 | def test_deploy_with_exclusions_should_remove_patterns_from_destination | |
71 | @config[:copy_exclude] = ".git" | |
72 | Dir.expects(:tmpdir).returns("/temp/dir") | |
73 | @source.expects(:checkout).with("154", "/temp/dir/1234567890").returns(:local_checkout) | |
74 | @strategy.expects(:system).with(:local_checkout) | |
75 | Dir.expects(:glob).with("/temp/dir/1234567890/.git", File::FNM_DOTMATCH).returns(%w(/temp/dir/1234567890/.git)) | |
76 | ||
77 | FileUtils.expects(:rm_rf).with(%w(/temp/dir/1234567890/.git)) | |
78 | prepare_standard_compress_and_copy! | |
79 | @strategy.deploy! | |
80 | end | |
81 | ||
82 | def test_deploy_with_exclusions_should_remove_glob_patterns_from_destination | |
83 | @config[:copy_exclude] = ".gi*" | |
84 | Dir.expects(:tmpdir).returns("/temp/dir") | |
85 | @source.expects(:checkout).with("154", "/temp/dir/1234567890").returns(:local_checkout) | |
86 | @strategy.expects(:system).with(:local_checkout) | |
87 | Dir.expects(:glob).with("/temp/dir/1234567890/.gi*", File::FNM_DOTMATCH).returns(%w(/temp/dir/1234567890/.git)) | |
88 | ||
89 | FileUtils.expects(:rm_rf).with(%w(/temp/dir/1234567890/.git)) | |
90 | prepare_standard_compress_and_copy! | |
91 | @strategy.deploy! | |
92 | end | |
93 | ||
94 | def test_deploy_with_export_should_use_tar_gz_and_export | |
95 | Dir.expects(:tmpdir).returns("/temp/dir") | |
96 | @config[:copy_strategy] = :export | |
97 | @source.expects(:export).with("154", "/temp/dir/1234567890").returns(:local_export) | |
98 | @strategy.expects(:system).with(:local_export) | |
99 | ||
100 | prepare_standard_compress_and_copy! | |
101 | @strategy.deploy! | |
102 | end | |
103 | ||
104 | def test_deploy_with_zip_should_use_zip_and_checkout | |
105 | Dir.expects(:tmpdir).returns("/temp/dir") | |
106 | Dir.expects(:chdir).with("/temp/dir").yields | |
107 | @config[:copy_compression] = :zip | |
108 | @source.expects(:checkout).with("154", "/temp/dir/1234567890").returns(:local_checkout) | |
109 | ||
110 | @strategy.expects(:system).with(:local_checkout) | |
111 | @strategy.expects(:system).with("zip -qyr 1234567890.zip 1234567890") | |
112 | @strategy.expects(:upload).with("/temp/dir/1234567890.zip", "/tmp/1234567890.zip") | |
113 | @strategy.expects(:run).with("cd /u/apps/test/releases && unzip -q /tmp/1234567890.zip && rm /tmp/1234567890.zip") | |
114 | ||
115 | mock_file = mock("file") | |
116 | mock_file.expects(:puts).with("154") | |
117 | File.expects(:open).with("/temp/dir/1234567890/REVISION", "w").yields(mock_file) | |
118 | ||
119 | FileUtils.expects(:rm).with("/temp/dir/1234567890.zip") | |
120 | FileUtils.expects(:rm_rf).with("/temp/dir/1234567890") | |
121 | ||
122 | @strategy.deploy! | |
123 | end | |
124 | ||
125 | def test_deploy_with_bzip2_should_use_bz2_and_checkout | |
126 | Dir.expects(:tmpdir).returns("/temp/dir") | |
127 | Dir.expects(:chdir).with("/temp/dir").yields | |
128 | @config[:copy_compression] = :bzip2 | |
129 | @source.expects(:checkout).with("154", "/temp/dir/1234567890").returns(:local_checkout) | |
130 | ||
131 | @strategy.expects(:system).with(:local_checkout) | |
132 | @strategy.expects(:system).with("tar cjf 1234567890.tar.bz2 1234567890") | |
133 | @strategy.expects(:upload).with("/temp/dir/1234567890.tar.bz2", "/tmp/1234567890.tar.bz2") | |
134 | @strategy.expects(:run).with("cd /u/apps/test/releases && tar xjf /tmp/1234567890.tar.bz2 && rm /tmp/1234567890.tar.bz2") | |
135 | ||
136 | mock_file = mock("file") | |
137 | mock_file.expects(:puts).with("154") | |
138 | File.expects(:open).with("/temp/dir/1234567890/REVISION", "w").yields(mock_file) | |
139 | ||
140 | FileUtils.expects(:rm).with("/temp/dir/1234567890.tar.bz2") | |
141 | FileUtils.expects(:rm_rf).with("/temp/dir/1234567890") | |
142 | ||
143 | @strategy.deploy! | |
144 | end | |
145 | ||
146 | def test_deploy_with_unknown_compression_type_should_error | |
147 | @config[:copy_compression] = :bogus | |
148 | Dir.expects(:tmpdir).returns("/temp/dir") | |
149 | @source.expects(:checkout).with("154", "/temp/dir/1234567890").returns(:local_checkout) | |
150 | @strategy.stubs(:system) | |
151 | File.stubs(:open) | |
152 | ||
153 | assert_raises(ArgumentError) { @strategy.deploy! } | |
154 | end | |
155 | ||
156 | def test_deploy_with_custom_copy_dir_should_use_that_as_tmpdir | |
157 | Dir.expects(:tmpdir).never | |
158 | Dir.expects(:chdir).with("/other/path").yields | |
159 | @config[:copy_dir] = "/other/path" | |
160 | @source.expects(:checkout).with("154", "/other/path/1234567890").returns(:local_checkout) | |
161 | ||
162 | @strategy.expects(:system).with(:local_checkout) | |
163 | @strategy.expects(:system).with("tar czf 1234567890.tar.gz 1234567890") | |
164 | @strategy.expects(:upload).with("/other/path/1234567890.tar.gz", "/tmp/1234567890.tar.gz") | |
165 | @strategy.expects(:run).with("cd /u/apps/test/releases && tar xzf /tmp/1234567890.tar.gz && rm /tmp/1234567890.tar.gz") | |
166 | ||
167 | mock_file = mock("file") | |
168 | mock_file.expects(:puts).with("154") | |
169 | File.expects(:open).with("/other/path/1234567890/REVISION", "w").yields(mock_file) | |
170 | ||
171 | FileUtils.expects(:rm).with("/other/path/1234567890.tar.gz") | |
172 | FileUtils.expects(:rm_rf).with("/other/path/1234567890") | |
173 | ||
174 | @strategy.deploy! | |
175 | end | |
176 | ||
177 | def test_deploy_with_copy_remote_dir_should_copy_to_that_dir | |
178 | @config[:copy_remote_dir] = "/somewhere/else" | |
179 | Dir.expects(:tmpdir).returns("/temp/dir") | |
180 | Dir.expects(:chdir).yields | |
181 | @source.expects(:checkout).returns(:local_checkout) | |
182 | ||
183 | @strategy.expects(:system).with(:local_checkout) | |
184 | @strategy.expects(:system).with("tar czf 1234567890.tar.gz 1234567890") | |
185 | @strategy.expects(:upload).with("/temp/dir/1234567890.tar.gz", "/somewhere/else/1234567890.tar.gz") | |
186 | @strategy.expects(:run).with("cd /u/apps/test/releases && tar xzf /somewhere/else/1234567890.tar.gz && rm /somewhere/else/1234567890.tar.gz") | |
187 | ||
188 | mock_file = mock("file") | |
189 | mock_file.expects(:puts).with("154") | |
190 | File.expects(:open).with("/temp/dir/1234567890/REVISION", "w").yields(mock_file) | |
191 | ||
192 | FileUtils.expects(:rm).with("/temp/dir/1234567890.tar.gz") | |
193 | FileUtils.expects(:rm_rf).with("/temp/dir/1234567890") | |
194 | ||
195 | @strategy.deploy! | |
196 | end | |
197 | ||
198 | def test_with_copy_cache_should_checkout_to_cache_if_cache_does_not_exist_and_then_copy | |
199 | @config[:copy_cache] = true | |
200 | ||
201 | Dir.stubs(:tmpdir).returns("/temp/dir") | |
202 | File.expects(:exists?).with("/temp/dir/captest").returns(false) | |
203 | Dir.expects(:chdir).with("/temp/dir/captest").yields | |
204 | ||
205 | @source.expects(:checkout).with("154", "/temp/dir/captest").returns(:local_checkout) | |
206 | @strategy.expects(:system).with(:local_checkout) | |
207 | ||
208 | FileUtils.expects(:mkdir_p).with("/temp/dir/1234567890") | |
209 | ||
210 | prepare_directory_tree!("/temp/dir/captest") | |
211 | ||
212 | prepare_standard_compress_and_copy! | |
213 | @strategy.deploy! | |
214 | end | |
215 | ||
216 | def test_with_copy_cache_should_update_cache_if_cache_exists_and_then_copy | |
217 | @config[:copy_cache] = true | |
218 | ||
219 | Dir.stubs(:tmpdir).returns("/temp/dir") | |
220 | File.expects(:exists?).with("/temp/dir/captest").returns(true) | |
221 | Dir.expects(:chdir).with("/temp/dir/captest").yields | |
222 | ||
223 | @source.expects(:sync).with("154", "/temp/dir/captest").returns(:local_sync) | |
224 | @strategy.expects(:system).with(:local_sync) | |
225 | ||
226 | FileUtils.expects(:mkdir_p).with("/temp/dir/1234567890") | |
227 | ||
228 | prepare_directory_tree!("/temp/dir/captest") | |
229 | ||
230 | prepare_standard_compress_and_copy! | |
231 | @strategy.deploy! | |
232 | end | |
233 | ||
234 | def test_with_copy_cache_with_custom_absolute_cache_dir_path_should_use_specified_cache_dir | |
235 | @config[:copy_cache] = "/u/caches/captest" | |
236 | ||
237 | Dir.stubs(:tmpdir).returns("/temp/dir") | |
238 | File.expects(:exists?).with("/u/caches/captest").returns(true) | |
239 | Dir.expects(:chdir).with("/u/caches/captest").yields | |
240 | ||
241 | @source.expects(:sync).with("154", "/u/caches/captest").returns(:local_sync) | |
242 | @strategy.expects(:system).with(:local_sync) | |
243 | ||
244 | FileUtils.expects(:mkdir_p).with("/temp/dir/1234567890") | |
245 | ||
246 | prepare_directory_tree!("/u/caches/captest") | |
247 | ||
248 | prepare_standard_compress_and_copy! | |
249 | @strategy.deploy! | |
250 | end | |
251 | ||
252 | def test_with_copy_cache_with_custom_relative_cache_dir_path_should_use_specified_cache_dir | |
253 | @config[:copy_cache] = "caches/captest" | |
254 | ||
255 | Dir.stubs(:pwd).returns("/u") | |
256 | Dir.stubs(:tmpdir).returns("/temp/dir") | |
257 | File.expects(:exists?).with("/u/caches/captest").returns(true) | |
258 | Dir.expects(:chdir).with("/u/caches/captest").yields | |
259 | ||
260 | @source.expects(:sync).with("154", "/u/caches/captest").returns(:local_sync) | |
261 | @strategy.expects(:system).with(:local_sync) | |
262 | ||
263 | FileUtils.expects(:mkdir_p).with("/temp/dir/1234567890") | |
264 | ||
265 | prepare_directory_tree!("/u/caches/captest") | |
266 | ||
267 | prepare_standard_compress_and_copy! | |
268 | @strategy.deploy! | |
269 | end | |
270 | ||
271 | def test_with_copy_cache_with_excludes_should_not_copy_excluded_files | |
272 | @config[:copy_cache] = true | |
273 | @config[:copy_exclude] = "*/bar.txt" | |
274 | ||
275 | Dir.stubs(:tmpdir).returns("/temp/dir") | |
276 | File.expects(:exists?).with("/temp/dir/captest").returns(true) | |
277 | Dir.expects(:chdir).with("/temp/dir/captest").yields | |
278 | ||
279 | @source.expects(:sync).with("154", "/temp/dir/captest").returns(:local_sync) | |
280 | @strategy.expects(:system).with(:local_sync) | |
281 | ||
282 | FileUtils.expects(:mkdir_p).with("/temp/dir/1234567890") | |
283 | ||
284 | prepare_directory_tree!("/temp/dir/captest", true) | |
285 | ||
286 | prepare_standard_compress_and_copy! | |
287 | @strategy.deploy! | |
288 | end | |
289 | ||
290 | def test_with_build_script_should_run_script | |
291 | @config[:build_script] = "mkdir bin" | |
292 | ||
293 | Dir.expects(:tmpdir).returns("/temp/dir") | |
294 | @source.expects(:checkout).with("154", "/temp/dir/1234567890").returns(:local_checkout) | |
295 | @strategy.expects(:system).with(:local_checkout) | |
296 | ||
297 | Dir.expects(:chdir).with("/temp/dir/1234567890").yields | |
298 | @strategy.expects(:system).with("mkdir bin") | |
299 | ||
300 | prepare_standard_compress_and_copy! | |
301 | @strategy.deploy! | |
302 | end | |
303 | ||
304 | private | |
305 | ||
306 | def prepare_directory_tree!(cache, exclude=false) | |
307 | Dir.expects(:glob).with("*", File::FNM_DOTMATCH).returns([".", "..", "app", "foo.txt"]) | |
308 | File.expects(:ftype).with("app").returns("directory") | |
309 | FileUtils.expects(:mkdir).with("/temp/dir/1234567890/app") | |
310 | File.expects(:ftype).with("foo.txt").returns("file") | |
311 | FileUtils.expects(:ln).with("foo.txt", "/temp/dir/1234567890/foo.txt") | |
312 | ||
313 | Dir.expects(:glob).with("app/*", File::FNM_DOTMATCH).returns(["app/.", "app/..", "app/bar.txt"]) | |
314 | ||
315 | unless exclude | |
316 | File.expects(:ftype).with("app/bar.txt").returns("file") | |
317 | FileUtils.expects(:ln).with("app/bar.txt", "/temp/dir/1234567890/app/bar.txt") | |
318 | end | |
319 | end | |
320 | ||
321 | def prepare_standard_compress_and_copy! | |
322 | Dir.expects(:chdir).with("/temp/dir").yields | |
323 | @strategy.expects(:system).with("tar czf 1234567890.tar.gz 1234567890") | |
324 | @strategy.expects(:upload).with("/temp/dir/1234567890.tar.gz", "/tmp/1234567890.tar.gz") | |
325 | @strategy.expects(:run).with("cd /u/apps/test/releases && tar xzf /tmp/1234567890.tar.gz && rm /tmp/1234567890.tar.gz") | |
326 | ||
327 | mock_file = mock("file") | |
328 | mock_file.expects(:puts).with("154") | |
329 | File.expects(:open).with("/temp/dir/1234567890/REVISION", "w").yields(mock_file) | |
330 | ||
331 | FileUtils.expects(:rm).with("/temp/dir/1234567890.tar.gz") | |
332 | FileUtils.expects(:rm_rf).with("/temp/dir/1234567890") | |
333 | end | |
334 | end |
0 | require "utils" | |
1 | require 'capistrano' | |
2 | ||
3 | class ExtensionsTest < Test::Unit::TestCase | |
4 | module CustomExtension | |
5 | def do_something(command) | |
6 | run(command) | |
7 | end | |
8 | end | |
9 | ||
10 | def setup | |
11 | @config = Capistrano::Configuration.new | |
12 | end | |
13 | ||
14 | def teardown | |
15 | Capistrano::EXTENSIONS.keys.each { |e| Capistrano.remove_plugin(e) } | |
16 | end | |
17 | ||
18 | def test_register_plugin_should_add_instance_method_on_configuration_and_return_true | |
19 | assert !@config.respond_to?(:custom_stuff) | |
20 | assert Capistrano.plugin(:custom_stuff, CustomExtension) | |
21 | assert @config.respond_to?(:custom_stuff) | |
22 | end | |
23 | ||
24 | def test_register_plugin_that_already_exists_should_return_false | |
25 | assert Capistrano.plugin(:custom_stuff, CustomExtension) | |
26 | assert !Capistrano.plugin(:custom_stuff, CustomExtension) | |
27 | end | |
28 | ||
29 | def test_register_plugin_with_public_method_name_should_fail | |
30 | method = Capistrano::Configuration.public_instance_methods.first | |
31 | assert_not_nil method, "need a public instance method for testing" | |
32 | assert_raises(Capistrano::Error) { Capistrano.plugin(method, CustomExtension) } | |
33 | end | |
34 | ||
35 | def test_register_plugin_with_protected_method_name_should_fail | |
36 | method = Capistrano::Configuration.protected_instance_methods.first | |
37 | assert_not_nil method, "need a protected instance method for testing" | |
38 | assert_raises(Capistrano::Error) { Capistrano.plugin(method, CustomExtension) } | |
39 | end | |
40 | ||
41 | def test_register_plugin_with_private_method_name_should_fail | |
42 | method = Capistrano::Configuration.private_instance_methods.first | |
43 | assert_not_nil method, "need a private instance method for testing" | |
44 | assert_raises(Capistrano::Error) { Capistrano.plugin(method, CustomExtension) } | |
45 | end | |
46 | ||
47 | def test_unregister_plugin_that_does_not_exist_should_return_false | |
48 | assert !Capistrano.remove_plugin(:custom_stuff) | |
49 | end | |
50 | ||
51 | def test_unregister_plugin_should_remove_method_and_return_true | |
52 | assert Capistrano.plugin(:custom_stuff, CustomExtension) | |
53 | assert @config.respond_to?(:custom_stuff) | |
54 | assert Capistrano.remove_plugin(:custom_stuff) | |
55 | assert !@config.respond_to?(:custom_stuff) | |
56 | end | |
57 | ||
58 | def test_registered_plugin_proxy_should_return_proxy_object | |
59 | Capistrano.plugin(:custom_stuff, CustomExtension) | |
60 | assert_instance_of Capistrano::ExtensionProxy, @config.custom_stuff | |
61 | end | |
62 | ||
63 | def test_proxy_object_should_delegate_to_configuration | |
64 | Capistrano.plugin(:custom_stuff, CustomExtension) | |
65 | @config.expects(:run).with("hello") | |
66 | @config.custom_stuff.do_something("hello") | |
67 | end | |
68 | end⏎ |
0 | role :test, "www.capistrano.test" | |
1 | ||
2 | task :testing, :roles => :test do | |
3 | set :testing_occurred, true | |
4 | end |
0 | set :application, "foo" | |
1 | set :repository, "1/2/#{application}" | |
2 | set :gateway, "#{__FILE__}.example.com" | |
3 | ||
4 | role :web, "www.example.com", :primary => true |
0 | require "utils" | |
1 | require 'capistrano/logger' | |
2 | require 'stringio' | |
3 | ||
4 | class LoggerTest < Test::Unit::TestCase | |
5 | def setup | |
6 | @io = StringIO.new | |
7 | @logger = Capistrano::Logger.new(:output => @io) | |
8 | end | |
9 | ||
10 | def test_logger_should_use_STDERR_by_default | |
11 | logger = Capistrano::Logger.new | |
12 | assert_equal STDERR, logger.device | |
13 | end | |
14 | ||
15 | def test_logger_should_use_output_option_if_output_responds_to_puts | |
16 | logger = Capistrano::Logger.new(:output => STDOUT) | |
17 | assert_equal STDOUT, logger.device | |
18 | end | |
19 | ||
20 | def test_logger_should_open_file_if_output_does_not_respond_to_puts | |
21 | File.expects(:open).with("logs/capistrano.log", "a").returns(:mock) | |
22 | logger = Capistrano::Logger.new(:output => "logs/capistrano.log") | |
23 | assert_equal :mock, logger.device | |
24 | end | |
25 | ||
26 | def test_close_should_not_close_device_if_device_is_default | |
27 | logger = Capistrano::Logger.new | |
28 | logger.device.expects(:close).never | |
29 | logger.close | |
30 | end | |
31 | ||
32 | def test_close_should_not_close_device_is_device_is_explicitly_given | |
33 | logger = Capistrano::Logger.new(:output => STDOUT) | |
34 | STDOUT.expects(:close).never | |
35 | logger.close | |
36 | end | |
37 | ||
38 | def test_close_should_close_device_when_device_was_implicitly_opened | |
39 | f = mock("file", :close => nil) | |
40 | File.expects(:open).with("logs/capistrano.log", "a").returns(f) | |
41 | logger = Capistrano::Logger.new(:output => "logs/capistrano.log") | |
42 | logger.close | |
43 | end | |
44 | ||
45 | def test_log_with_level_greater_than_threshold_should_ignore_message | |
46 | @logger.level = 3 | |
47 | @logger.log(4, "message") | |
48 | assert @io.string.empty? | |
49 | end | |
50 | ||
51 | def test_log_with_level_equal_to_threshold_should_log_message | |
52 | @logger.level = 3 | |
53 | @logger.log(3, "message") | |
54 | assert @io.string.include?("message") | |
55 | end | |
56 | ||
57 | def test_log_with_level_less_than_threshold_should_log_message | |
58 | @logger.level = 3 | |
59 | @logger.log(2, "message") | |
60 | assert @io.string.include?("message") | |
61 | end | |
62 | ||
63 | def test_log_with_multiline_message_should_log_each_line_separately | |
64 | @logger.log(0, "first line\nsecond line") | |
65 | assert @io.string.include?("*** first line") | |
66 | assert @io.string.include?("*** second line") | |
67 | end | |
68 | ||
69 | def test_log_with_line_prefix_should_insert_line_prefix_before_message | |
70 | @logger.log(0, "message", "prefix") | |
71 | assert @io.string.include?("*** [prefix] message") | |
72 | end | |
73 | ||
74 | def test_log_with_level_0_should_have_strong_indent | |
75 | @logger.log(0, "message") | |
76 | assert @io.string.match(/^\*\*\* message/) | |
77 | end | |
78 | ||
79 | def test_log_with_level_1_should_have_weaker_indent | |
80 | @logger.level = 1 | |
81 | @logger.log(1, "message") | |
82 | assert @io.string.match(/^ \*\* message/) | |
83 | end | |
84 | ||
85 | def test_log_with_level_2_should_have_weaker_indent | |
86 | @logger.level = 2 | |
87 | @logger.log(2, "message") | |
88 | assert @io.string.match(/^ \* message/) | |
89 | end | |
90 | ||
91 | def test_log_with_level_3_should_have_weakest_indent | |
92 | @logger.level = 3 | |
93 | @logger.log(3, "message") | |
94 | assert @io.string.match(/^ message/) | |
95 | end | |
96 | ||
97 | def test_important_should_delegate_to_log_with_level_IMPORTANT | |
98 | @logger.expects(:log).with(Capistrano::Logger::IMPORTANT, "message", "prefix") | |
99 | @logger.important("message", "prefix") | |
100 | end | |
101 | ||
102 | def test_info_should_delegate_to_log_with_level_INFO | |
103 | @logger.expects(:log).with(Capistrano::Logger::INFO, "message", "prefix") | |
104 | @logger.info("message", "prefix") | |
105 | end | |
106 | ||
107 | def test_debug_should_delegate_to_log_with_level_DEBUG | |
108 | @logger.expects(:log).with(Capistrano::Logger::DEBUG, "message", "prefix") | |
109 | @logger.debug("message", "prefix") | |
110 | end | |
111 | ||
112 | def test_trace_should_delegate_to_log_with_level_TRACE | |
113 | @logger.expects(:log).with(Capistrano::Logger::TRACE, "message", "prefix") | |
114 | @logger.trace("message", "prefix") | |
115 | end | |
116 | ||
117 | def test_ordering_of_levels | |
118 | assert Capistrano::Logger::IMPORTANT < Capistrano::Logger::INFO | |
119 | assert Capistrano::Logger::INFO < Capistrano::Logger::DEBUG | |
120 | assert Capistrano::Logger::DEBUG < Capistrano::Logger::TRACE | |
121 | end | |
122 | end |
0 | require 'utils' | |
1 | require 'capistrano/configuration' | |
2 | ||
3 | class RecipesTest < Test::Unit::TestCase | |
4 | ||
5 | def setup | |
6 | @config = Capistrano::Configuration.new | |
7 | @config.stubs(:logger).returns(stub_everything) | |
8 | end | |
9 | ||
10 | def test_current_releases_does_not_cause_error_on_dry_run | |
11 | @config.dry_run = true | |
12 | @config.load 'deploy' | |
13 | @config.load do | |
14 | set :application, "foo" | |
15 | task :dry_run_test do | |
16 | fetch :current_release | |
17 | end | |
18 | end | |
19 | ||
20 | assert_nothing_raised do | |
21 | @config.dry_run_test | |
22 | end | |
23 | end | |
24 | end⏎ |
0 | require "utils" | |
1 | require 'capistrano/role' | |
2 | ||
3 | class RoleTest < Test::Unit::TestCase | |
4 | def test_clearing_a_populated_role_should_yield_no_servers | |
5 | role = Capistrano::Role.new("app1.capistrano.test", lambda { |o| "app2.capistrano.test" }) | |
6 | assert_equal 2, role.servers.size | |
7 | role.clear | |
8 | assert role.servers.empty? | |
9 | end | |
10 | end |
0 | require "utils" | |
1 | require 'capistrano/server_definition' | |
2 | ||
3 | class ServerDefinitionTest < Test::Unit::TestCase | |
4 | def test_new_without_credentials_or_port_should_set_values_to_defaults | |
5 | server = Capistrano::ServerDefinition.new("www.capistrano.test") | |
6 | assert_equal "www.capistrano.test", server.host | |
7 | assert_nil server.user | |
8 | assert_nil server.port | |
9 | end | |
10 | ||
11 | def test_new_with_encoded_user_should_extract_user_and_use_default_port | |
12 | server = Capistrano::ServerDefinition.new("jamis@www.capistrano.test") | |
13 | assert_equal "www.capistrano.test", server.host | |
14 | assert_equal "jamis", server.user | |
15 | assert_nil server.port | |
16 | end | |
17 | ||
18 | def test_new_with_encoded_port_should_extract_port_and_use_default_user | |
19 | server = Capistrano::ServerDefinition.new("www.capistrano.test:8080") | |
20 | assert_equal "www.capistrano.test", server.host | |
21 | assert_nil server.user | |
22 | assert_equal 8080, server.port | |
23 | end | |
24 | ||
25 | def test_new_with_encoded_user_and_port_should_extract_user_and_port | |
26 | server = Capistrano::ServerDefinition.new("jamis@www.capistrano.test:8080") | |
27 | assert_equal "www.capistrano.test", server.host | |
28 | assert_equal "jamis", server.user | |
29 | assert_equal 8080, server.port | |
30 | end | |
31 | ||
32 | def test_new_with_user_as_option_should_use_given_user | |
33 | server = Capistrano::ServerDefinition.new("www.capistrano.test", :user => "jamis") | |
34 | assert_equal "www.capistrano.test", server.host | |
35 | assert_equal "jamis", server.user | |
36 | assert_nil server.port | |
37 | end | |
38 | ||
39 | def test_new_with_port_as_option_should_use_given_user | |
40 | server = Capistrano::ServerDefinition.new("www.capistrano.test", :port => 8080) | |
41 | assert_equal "www.capistrano.test", server.host | |
42 | assert_nil server.user | |
43 | assert_equal 8080, server.port | |
44 | end | |
45 | ||
46 | def test_encoded_value_should_override_hash_option | |
47 | server = Capistrano::ServerDefinition.new("jamis@www.capistrano.test:8080", :user => "david", :port => 8081) | |
48 | assert_equal "www.capistrano.test", server.host | |
49 | assert_equal "jamis", server.user | |
50 | assert_equal 8080, server.port | |
51 | assert server.options.empty? | |
52 | end | |
53 | ||
54 | def test_new_with_option_should_dup_option_hash | |
55 | options = {} | |
56 | server = Capistrano::ServerDefinition.new("www.capistrano.test", options) | |
57 | assert_not_equal options.object_id, server.options.object_id | |
58 | end | |
59 | ||
60 | def test_new_with_options_should_keep_options | |
61 | server = Capistrano::ServerDefinition.new("www.capistrano.test", :primary => true) | |
62 | assert_equal true, server.options[:primary] | |
63 | end | |
64 | ||
65 | def test_default_user_should_try_to_guess_username | |
66 | ENV.stubs(:[]).returns(nil) | |
67 | assert_equal "not-specified", Capistrano::ServerDefinition.default_user | |
68 | ||
69 | ENV.stubs(:[]).returns(nil) | |
70 | ENV.stubs(:[]).with("USERNAME").returns("ryan") | |
71 | assert_equal "ryan", Capistrano::ServerDefinition.default_user | |
72 | ||
73 | ENV.stubs(:[]).returns(nil) | |
74 | ENV.stubs(:[]).with("USER").returns("jamis") | |
75 | assert_equal "jamis", Capistrano::ServerDefinition.default_user | |
76 | end | |
77 | ||
78 | def test_comparison_should_match_when_host_user_port_are_same | |
79 | s1 = server("jamis@www.capistrano.test:8080") | |
80 | s2 = server("www.capistrano.test", :user => "jamis", :port => 8080) | |
81 | assert_equal s1, s2 | |
82 | assert_equal s1.hash, s2.hash | |
83 | assert s1.eql?(s2) | |
84 | end | |
85 | ||
86 | def test_servers_should_be_comparable | |
87 | s1 = server("jamis@www.capistrano.test:8080") | |
88 | s2 = server("www.alphabet.test:1234") | |
89 | s3 = server("jamis@www.capistrano.test:8075") | |
90 | s4 = server("billy@www.capistrano.test:8080") | |
91 | ||
92 | assert s2 < s1 | |
93 | assert s3 < s1 | |
94 | assert s4 < s1 | |
95 | assert s2 < s3 | |
96 | assert s2 < s4 | |
97 | assert s3 < s4 | |
98 | end | |
99 | ||
100 | def test_comparison_should_not_match_when_any_of_host_user_port_differ | |
101 | s1 = server("jamis@www.capistrano.test:8080") | |
102 | s2 = server("bob@www.capistrano.test:8080") | |
103 | s3 = server("jamis@www.capistrano.test:8081") | |
104 | s4 = server("jamis@app.capistrano.test:8080") | |
105 | assert_not_equal s1, s2 | |
106 | assert_not_equal s1, s3 | |
107 | assert_not_equal s1, s4 | |
108 | assert_not_equal s2, s3 | |
109 | assert_not_equal s2, s4 | |
110 | assert_not_equal s3, s4 | |
111 | end | |
112 | ||
113 | def test_to_s | |
114 | assert_equal "www.capistrano.test", server("www.capistrano.test").to_s | |
115 | assert_equal "www.capistrano.test", server("www.capistrano.test:22").to_s | |
116 | assert_equal "www.capistrano.test:1234", server("www.capistrano.test:1234").to_s | |
117 | assert_equal "jamis@www.capistrano.test", server("jamis@www.capistrano.test").to_s | |
118 | assert_equal "jamis@www.capistrano.test:1234", server("jamis@www.capistrano.test:1234").to_s | |
119 | end | |
120 | end |
0 | require "utils" | |
1 | require 'capistrano/configuration' | |
2 | require 'capistrano/shell' | |
3 | ||
4 | class ShellTest < Test::Unit::TestCase | |
5 | def setup | |
6 | @config = Capistrano::Configuration.new | |
7 | @shell = Capistrano::Shell.new(@config) | |
8 | @shell.stubs(:puts) | |
9 | end | |
10 | ||
11 | def test_readline_fallback_prompt_should_write_to_stdout_and_read_from_stdin | |
12 | STDOUT.expects(:print).with("prompt> ") | |
13 | STDOUT.expects(:flush) | |
14 | STDIN.expects(:gets).returns("hi\n") | |
15 | assert_equal "hi\n", Capistrano::Shell::ReadlineFallback.readline("prompt> ") | |
16 | end | |
17 | ||
18 | def test_question_mark_as_input_should_trigger_help | |
19 | @shell.expects(:read_line).returns("?") | |
20 | @shell.expects(:help) | |
21 | assert @shell.read_and_execute | |
22 | end | |
23 | ||
24 | def test_help_as_input_should_trigger_help | |
25 | @shell.expects(:read_line).returns("help") | |
26 | @shell.expects(:help) | |
27 | assert @shell.read_and_execute | |
28 | end | |
29 | ||
30 | def test_quit_as_input_should_cause_read_and_execute_to_return_false | |
31 | @shell.expects(:read_line).returns("quit") | |
32 | assert !@shell.read_and_execute | |
33 | end | |
34 | ||
35 | def test_exit_as_input_should_cause_read_and_execute_to_return_false | |
36 | @shell.expects(:read_line).returns("exit") | |
37 | assert !@shell.read_and_execute | |
38 | end | |
39 | ||
40 | def test_set_should_parse_flag_and_value_and_call_set_option | |
41 | @shell.expects(:read_line).returns("set -v 5") | |
42 | @shell.expects(:set_option).with("v", "5") | |
43 | assert @shell.read_and_execute | |
44 | end | |
45 | ||
46 | def test_text_without_with_or_on_gets_processed_verbatim | |
47 | @shell.expects(:read_line).returns("hello world") | |
48 | @shell.expects(:process_command).with(nil, nil, "hello world") | |
49 | assert @shell.read_and_execute | |
50 | end | |
51 | ||
52 | def test_text_with_with_gets_processed_with_with # lol | |
53 | @shell.expects(:read_line).returns("with app,db hello world") | |
54 | @shell.expects(:process_command).with("with", "app,db", "hello world") | |
55 | assert @shell.read_and_execute | |
56 | end | |
57 | ||
58 | def test_text_with_on_gets_processed_with_on | |
59 | @shell.expects(:read_line).returns("on app,db hello world") | |
60 | @shell.expects(:process_command).with("on", "app,db", "hello world") | |
61 | assert @shell.read_and_execute | |
62 | end | |
63 | ||
64 | def test_task_command_with_bang_gets_processed_by_exec_tasks | |
65 | while_testing_post_exec_commands do | |
66 | @shell.expects(:read_line).returns("!deploy") | |
67 | @shell.expects(:exec_tasks).with(["deploy"]) | |
68 | assert @shell.read_and_execute | |
69 | end | |
70 | end | |
71 | ||
72 | def test_normal_command_gets_processed_by_exec_command | |
73 | while_testing_post_exec_commands do | |
74 | @shell.expects(:read_line).returns("uptime") | |
75 | @shell.expects(:exec_command).with("uptime",nil) | |
76 | @shell.expects(:connect) | |
77 | assert @shell.read_and_execute | |
78 | end | |
79 | end | |
80 | ||
81 | ||
82 | private | |
83 | ||
84 | def while_testing_post_exec_commands(&block) | |
85 | @shell.instance_variable_set(:@mutex,Mutex.new) | |
86 | yield | |
87 | end | |
88 | ||
89 | end |
0 | require "utils" | |
1 | require 'capistrano/ssh' | |
2 | ||
3 | class SSHTest < Test::Unit::TestCase | |
4 | def setup | |
5 | Capistrano::ServerDefinition.stubs(:default_user).returns("default-user") | |
6 | @options = { :password => nil, | |
7 | :auth_methods => %w(publickey hostbased), | |
8 | :config => false } | |
9 | @server = server("capistrano") | |
10 | Net::SSH.stubs(:configuration_for).returns({}) | |
11 | end | |
12 | ||
13 | def test_connect_with_bare_server_without_options_or_config_with_public_key_succeeding_should_only_loop_once | |
14 | Net::SSH.expects(:start).with(@server.host, "default-user", @options).returns(success = Object.new) | |
15 | assert_equal success, Capistrano::SSH.connect(@server) | |
16 | end | |
17 | ||
18 | def test_connect_with_bare_server_without_options_with_public_key_failing_should_try_password | |
19 | Net::SSH.expects(:start).with(@server.host, "default-user", @options).raises(Net::SSH::AuthenticationFailed) | |
20 | Net::SSH.expects(:start).with(@server.host, "default-user", @options.merge(:password => "f4b13n", :auth_methods => %w(password keyboard-interactive))).returns(success = Object.new) | |
21 | assert_equal success, Capistrano::SSH.connect(@server, :password => "f4b13n") | |
22 | end | |
23 | ||
24 | def test_connect_with_bare_server_without_options_public_key_and_password_failing_should_raise_error | |
25 | Net::SSH.expects(:start).with(@server.host, "default-user", @options).raises(Net::SSH::AuthenticationFailed) | |
26 | Net::SSH.expects(:start).with(@server.host, "default-user", @options.merge(:password => "f4b13n", :auth_methods => %w(password keyboard-interactive))).raises(Net::SSH::AuthenticationFailed) | |
27 | assert_raises(Net::SSH::AuthenticationFailed) do | |
28 | Capistrano::SSH.connect(@server, :password => "f4b13n") | |
29 | end | |
30 | end | |
31 | ||
32 | def test_connect_with_bare_server_and_user_via_public_key_should_pass_user_to_net_ssh | |
33 | Net::SSH.expects(:start).with(@server.host, "jamis", @options).returns(success = Object.new) | |
34 | assert_equal success, Capistrano::SSH.connect(@server, :user => "jamis") | |
35 | end | |
36 | ||
37 | def test_connect_with_bare_server_and_user_via_password_should_pass_user_to_net_ssh | |
38 | Net::SSH.expects(:start).with(@server.host, "jamis", @options).raises(Net::SSH::AuthenticationFailed) | |
39 | Net::SSH.expects(:start).with(@server.host, "jamis", @options.merge(:password => "f4b13n", :auth_methods => %w(password keyboard-interactive))).returns(success = Object.new) | |
40 | assert_equal success, Capistrano::SSH.connect(@server, :user => "jamis", :password => "f4b13n") | |
41 | end | |
42 | ||
43 | def test_connect_with_bare_server_with_explicit_port_should_pass_port_to_net_ssh | |
44 | Net::SSH.expects(:start).with(@server.host, "default-user", @options.merge(:port => 1234)).returns(success = Object.new) | |
45 | assert_equal success, Capistrano::SSH.connect(@server, :port => 1234) | |
46 | end | |
47 | ||
48 | def test_connect_with_server_with_user_should_pass_user_to_net_ssh | |
49 | server = server("jamis@capistrano") | |
50 | Net::SSH.expects(:start).with(server.host, "jamis", @options).returns(success = Object.new) | |
51 | assert_equal success, Capistrano::SSH.connect(server) | |
52 | end | |
53 | ||
54 | def test_connect_with_server_with_port_should_pass_port_to_net_ssh | |
55 | server = server("capistrano:1235") | |
56 | Net::SSH.expects(:start).with(server.host, "default-user", @options.merge(:port => 1235)).returns(success = Object.new) | |
57 | assert_equal success, Capistrano::SSH.connect(server) | |
58 | end | |
59 | ||
60 | def test_connect_with_server_with_user_and_port_should_pass_user_and_port_to_net_ssh | |
61 | server = server("jamis@capistrano:1235") | |
62 | Net::SSH.expects(:start).with(server.host, "jamis", @options.merge(:port => 1235)).returns(success = Object.new) | |
63 | assert_equal success, Capistrano::SSH.connect(server) | |
64 | end | |
65 | ||
66 | def test_connect_with_server_with_other_ssh_options_should_pass_ssh_options_to_net_ssh | |
67 | server = server("jamis@capistrano:1235", :ssh_options => { :keys => %w(some_valid_key), :auth_methods => %w(a_method), :hmac => 'none' }) | |
68 | Net::SSH.expects(:start).with(server.host, "jamis", @options.merge(:port => 1235, :keys => %w(some_valid_key), :auth_methods => %w(a_method), :hmac => 'none' )).returns(success = Object.new) | |
69 | assert_equal success, Capistrano::SSH.connect(server) | |
70 | end | |
71 | ||
72 | def test_connect_with_ssh_options_should_use_ssh_options | |
73 | ssh_options = { :username => "JamisMan", :port => 8125, :config => false } | |
74 | Net::SSH.expects(:start).with(@server.host, "JamisMan", @options.merge(:port => 8125, :config => false)).returns(success = Object.new) | |
75 | assert_equal success, Capistrano::SSH.connect(@server, {:ssh_options => ssh_options}) | |
76 | end | |
77 | ||
78 | def test_connect_with_options_and_ssh_options_should_see_options_override_ssh_options | |
79 | ssh_options = { :username => "JamisMan", :port => 8125, :forward_agent => true } | |
80 | Net::SSH.expects(:start).with(@server.host, "jamis", @options.merge(:port => 1235, :forward_agent => true)).returns(success = Object.new) | |
81 | assert_equal success, Capistrano::SSH.connect(@server, :ssh_options => ssh_options, :user => "jamis", :port => 1235) | |
82 | end | |
83 | ||
84 | def test_connect_with_verbose_option_should_set_verbose_option_on_ssh | |
85 | Net::SSH.expects(:start).with(@server.host, "default-user", @options).returns(success = Object.new) | |
86 | assert_equal success, Capistrano::SSH.connect(@server, :verbose => 0) | |
87 | Net::SSH.expects(:start).with(@server.host, "default-user", @options.merge(:verbose => :debug)).returns(success = Object.new) | |
88 | assert_equal success, Capistrano::SSH.connect(@server, :verbose => 1) | |
89 | Net::SSH.expects(:start).with(@server.host, "default-user", @options.merge(:verbose => :debug)).returns(success = Object.new) | |
90 | assert_equal success, Capistrano::SSH.connect(@server, :verbose => 2) | |
91 | end | |
92 | ||
93 | def test_connect_with_ssh_options_should_see_server_options_override_ssh_options | |
94 | ssh_options = { :username => "JamisMan", :port => 8125, :forward_agent => true } | |
95 | server = server("jamis@capistrano:1235") | |
96 | Net::SSH.expects(:start).with(server.host, "jamis", @options.merge(:port => 1235, :forward_agent => true, :config => false)).returns(success = Object.new) | |
97 | assert_equal success, Capistrano::SSH.connect(server, {:ssh_options => ssh_options}) | |
98 | end | |
99 | ||
100 | def test_connect_should_add_xserver_accessor_to_connection | |
101 | Net::SSH.expects(:start).with(@server.host, "default-user", @options).returns(success = Object.new) | |
102 | assert_equal success, Capistrano::SSH.connect(@server) | |
103 | assert success.respond_to?(:xserver) | |
104 | assert success.respond_to?(:xserver) | |
105 | assert_equal success.xserver, @server | |
106 | end | |
107 | ||
108 | def test_connect_should_not_retry_if_custom_auth_methods_are_given | |
109 | Net::SSH.expects(:start).with(@server.host, "default-user", @options.merge(:auth_methods => %w(publickey))).raises(Net::SSH::AuthenticationFailed) | |
110 | assert_raises(Net::SSH::AuthenticationFailed) { Capistrano::SSH.connect(@server, :ssh_options => { :auth_methods => %w(publickey) }) } | |
111 | end | |
112 | end |
0 | require "utils" | |
1 | require 'capistrano/task_definition' | |
2 | ||
3 | # Silences the wanrnings raised in the two deprecation tests | |
4 | $VERBOSE = nil | |
5 | ||
6 | class TaskDefinitionTest < Test::Unit::TestCase | |
7 | def setup | |
8 | @namespace = namespace | |
9 | end | |
10 | ||
11 | def test_fqn_at_top_level_should_be_task_name | |
12 | task = new_task(:testing) | |
13 | assert_equal "testing", task.fully_qualified_name | |
14 | end | |
15 | ||
16 | def test_fqn_in_namespace_should_include_namespace_fqn | |
17 | ns = namespace("outer:inner") | |
18 | task = new_task(:testing, ns) | |
19 | assert_equal "outer:inner:testing", task.fully_qualified_name | |
20 | end | |
21 | ||
22 | def test_fqn_at_top_level_when_default_should_be_default | |
23 | task = new_task(:default) | |
24 | assert_equal "default", task.fully_qualified_name | |
25 | end | |
26 | ||
27 | def test_name_should_change_task_name | |
28 | task = new_task(:foo) | |
29 | task.name = :bar | |
30 | ||
31 | assert_equal :bar, task.name | |
32 | end | |
33 | ||
34 | def test_raise_an_exception_when_task_names_can_not_be_converted | |
35 | task = new_task('valid task name') | |
36 | ||
37 | assert_raises(ArgumentError) { task.name = ['invalid task name'] } | |
38 | end | |
39 | ||
40 | def test_fqn_in_namespace_when_default_should_be_namespace_fqn | |
41 | ns = namespace("outer:inner") | |
42 | task = new_task(:default, ns) | |
43 | ns.stubs(:default_task => task) | |
44 | assert_equal "outer:inner", task.fully_qualified_name | |
45 | end | |
46 | ||
47 | def test_task_should_require_block | |
48 | assert_raises(ArgumentError) do | |
49 | Capistrano::TaskDefinition.new(:testing, @namespace) | |
50 | end | |
51 | end | |
52 | ||
53 | def test_description_should_return_empty_string_if_not_given | |
54 | assert_equal "", new_task(:testing).description | |
55 | end | |
56 | ||
57 | def test_description_should_return_desc_attribute | |
58 | assert_equal "something", new_task(:testing, @namespace, :desc => "something").description | |
59 | end | |
60 | ||
61 | def test_description_should_strip_leading_and_trailing_whitespace | |
62 | assert_equal "something", new_task(:testing, @namespace, :desc => " something ").description | |
63 | end | |
64 | ||
65 | def test_description_should_normalize_newlines | |
66 | assert_equal "a\nb\nc", new_task(:testing, @namespace, :desc => "a\nb\r\nc").description | |
67 | end | |
68 | ||
69 | def test_description_should_detect_and_remove_indentation | |
70 | desc = <<-DESC | |
71 | Here is some indented text \ | |
72 | and I want all of this to \ | |
73 | run together on a single line, \ | |
74 | without any extraneous spaces. | |
75 | ||
76 | additional indentation will | |
77 | be preserved. | |
78 | DESC | |
79 | ||
80 | task = new_task(:testing, @namespace, :desc => desc) | |
81 | assert_equal "Here is some indented text and I want all of this to run together on a single line, without any extraneous spaces.\n\n additional indentation will\n be preserved.", task.description | |
82 | end | |
83 | ||
84 | def test_description_munging_should_be_sensitive_to_code_blocks | |
85 | desc = <<-DESC | |
86 | Here is a line \ | |
87 | wrapped with spacing in it. | |
88 | ||
89 | foo bar | |
90 | baz bang | |
91 | DESC | |
92 | ||
93 | task = new_task(:testing, @namespace, :desc => desc) | |
94 | assert_equal "Here is a line wrapped with spacing in it.\n\n foo bar\n baz bang", task.description | |
95 | end | |
96 | ||
97 | def test_brief_description_should_return_first_sentence_in_description | |
98 | desc = "This is the task. It does all kinds of things." | |
99 | task = new_task(:testing, @namespace, :desc => desc) | |
100 | assert_equal "This is the task.", task.brief_description | |
101 | end | |
102 | ||
103 | def test_brief_description_should_truncate_if_length_given | |
104 | desc = "This is the task that does all kinds of things. And then some." | |
105 | task = new_task(:testing, @namespace, :desc => desc) | |
106 | assert_equal "This is the task ...", task.brief_description(20) | |
107 | end | |
108 | ||
109 | def test_brief_description_should_not_break_at_period_in_middle_of_sentence | |
110 | task = new_task(:testing, @namespace, :desc => "Take file.txt and copy it.") | |
111 | assert_equal "Take file.txt and copy it.", task.brief_description | |
112 | ||
113 | task = new_task(:testing, @namespace, :desc => "Take file.txt and copy it. Then do something else.") | |
114 | assert_equal "Take file.txt and copy it.", task.brief_description | |
115 | end | |
116 | end |
0 | require 'utils' | |
1 | require 'capistrano/transfer' | |
2 | ||
3 | class TransferTest < Test::Unit::TestCase | |
4 | def test_class_process_should_delegate_to_instance_process | |
5 | Capistrano::Transfer.expects(:new).with(:up, "from", "to", %w(a b c), {}).returns(mock('transfer', :process! => nil)).yields | |
6 | yielded = false | |
7 | Capistrano::Transfer.process(:up, "from", "to", %w(a b c), {}) { yielded = true } | |
8 | assert yielded | |
9 | end | |
10 | ||
11 | def test_default_transport_is_sftp | |
12 | transfer = Capistrano::Transfer.new(:up, "from", "to", []) | |
13 | assert_equal :sftp, transfer.transport | |
14 | end | |
15 | ||
16 | def test_active_is_true_when_any_sftp_transfers_are_active | |
17 | returns = [false, false, true] | |
18 | sessions = [session('app1', :sftp), session('app2', :sftp), session('app3', :sftp)].each { |s| s.xsftp.expects(:upload).returns(stub('operation', :active? => returns.shift)) } | |
19 | transfer = Capistrano::Transfer.new(:up, "from", "to", sessions, :via => :sftp) | |
20 | assert_equal true, transfer.active? | |
21 | end | |
22 | ||
23 | def test_active_is_false_when_all_sftp_transfers_are_not_active | |
24 | sessions = [session('app1', :sftp), session('app2', :sftp)].each { |s| s.xsftp.expects(:upload).returns(stub('operation', :active? => false)) } | |
25 | transfer = Capistrano::Transfer.new(:up, "from", "to", sessions, :via => :sftp) | |
26 | assert_equal false, transfer.active? | |
27 | end | |
28 | ||
29 | def test_active_is_true_when_any_scp_transfers_are_active | |
30 | returns = [false, false, true] | |
31 | sessions = [session('app1', :scp), session('app2', :scp), session('app3', :scp)].each do |s| | |
32 | channel = stub('channel', :[]= => nil, :active? => returns.shift) | |
33 | s.scp.expects(:upload).returns(channel) | |
34 | end | |
35 | transfer = Capistrano::Transfer.new(:up, "from", "to", sessions, :via => :scp) | |
36 | assert_equal true, transfer.active? | |
37 | end | |
38 | ||
39 | def test_active_is_false_when_all_scp_transfers_are_not_active | |
40 | sessions = [session('app1', :scp), session('app2', :scp), session('app3', :scp)].each do |s| | |
41 | channel = stub('channel', :[]= => nil, :active? => false) | |
42 | s.scp.expects(:upload).returns(channel) | |
43 | end | |
44 | transfer = Capistrano::Transfer.new(:up, "from", "to", sessions, :via => :scp) | |
45 | assert_equal false, transfer.active? | |
46 | end | |
47 | ||
48 | [:up, :down].each do |direction| | |
49 | define_method("test_sftp_#{direction}load_from_file_to_file_should_normalize_from_and_to") do | |
50 | sessions = [session('app1', :sftp), session('app2', :sftp)] | |
51 | ||
52 | sessions.each do |session| | |
53 | session.xsftp.expects("#{direction}load".to_sym).with("from-#{session.xserver.host}", "to-#{session.xserver.host}", | |
54 | :properties => { :server => session.xserver, :host => session.xserver.host }) | |
55 | end | |
56 | ||
57 | transfer = Capistrano::Transfer.new(direction, "from-$CAPISTRANO:HOST$", "to-$CAPISTRANO:HOST$", sessions) | |
58 | end | |
59 | ||
60 | define_method("test_scp_#{direction}load_from_file_to_file_should_normalize_from_and_to") do | |
61 | sessions = [session('app1', :scp), session('app2', :scp)] | |
62 | ||
63 | sessions.each do |session| | |
64 | session.scp.expects("#{direction}load".to_sym).returns({}).with("from-#{session.xserver.host}", "to-#{session.xserver.host}", :via => :scp) | |
65 | end | |
66 | ||
67 | transfer = Capistrano::Transfer.new(direction, "from-$CAPISTRANO:HOST$", "to-$CAPISTRANO:HOST$", sessions, :via => :scp) | |
68 | end | |
69 | end | |
70 | ||
71 | def test_sftp_upload_from_IO_to_file_should_clone_the_IO_for_each_connection | |
72 | sessions = [session('app1', :sftp), session('app2', :sftp)] | |
73 | io = StringIO.new("from here") | |
74 | ||
75 | sessions.each do |session| | |
76 | session.xsftp.expects(:upload).with do |from, to, opts| | |
77 | from != io && from.is_a?(StringIO) && from.string == io.string && | |
78 | to == "/to/here-#{session.xserver.host}" && | |
79 | opts[:properties][:server] == session.xserver && | |
80 | opts[:properties][:host] == session.xserver.host | |
81 | end | |
82 | end | |
83 | ||
84 | transfer = Capistrano::Transfer.new(:up, StringIO.new("from here"), "/to/here-$CAPISTRANO:HOST$", sessions) | |
85 | end | |
86 | ||
87 | def test_scp_upload_from_IO_to_file_should_clone_the_IO_for_each_connection | |
88 | sessions = [session('app1', :scp), session('app2', :scp)] | |
89 | io = StringIO.new("from here") | |
90 | ||
91 | sessions.each do |session| | |
92 | channel = mock('channel') | |
93 | channel.expects(:[]=).with(:server, session.xserver) | |
94 | channel.expects(:[]=).with(:host, session.xserver.host) | |
95 | session.scp.expects(:upload).returns(channel).with do |from, to, opts| | |
96 | from != io && from.is_a?(StringIO) && from.string == io.string && | |
97 | to == "/to/here-#{session.xserver.host}" | |
98 | end | |
99 | end | |
100 | ||
101 | transfer = Capistrano::Transfer.new(:up, StringIO.new("from here"), "/to/here-$CAPISTRANO:HOST$", sessions, :via => :scp) | |
102 | end | |
103 | ||
104 | def test_process_should_block_until_transfer_is_no_longer_active | |
105 | transfer = Capistrano::Transfer.new(:up, "from", "to", []) | |
106 | transfer.expects(:process_iteration).times(4).yields.returns(true,true,true,false) | |
107 | transfer.expects(:active?).times(4) | |
108 | transfer.process! | |
109 | end | |
110 | ||
111 | def test_errors_raised_for_a_sftp_session_should_abort_session_and_continue_with_remaining_sessions | |
112 | s = session('app1') | |
113 | error = ExceptionWithSession.new(s) | |
114 | transfer = Capistrano::Transfer.new(:up, "from", "to", []) | |
115 | transfer.expects(:process_iteration).raises(error).times(3).returns(true, false) | |
116 | txfr = mock('transfer', :abort! => true) | |
117 | txfr.expects(:[]=).with(:failed, true) | |
118 | txfr.expects(:[]=).with(:error, error) | |
119 | transfer.expects(:session_map).returns(s => txfr) | |
120 | transfer.process! | |
121 | end | |
122 | ||
123 | def test_errors_raised_for_a_scp_session_should_abort_session_and_continue_with_remaining_sessions | |
124 | s = session('app1') | |
125 | error = ExceptionWithSession.new(s) | |
126 | transfer = Capistrano::Transfer.new(:up, "from", "to", [], :via => :scp) | |
127 | transfer.expects(:process_iteration).raises(error).times(3).returns(true, false) | |
128 | txfr = mock('channel', :close => true) | |
129 | txfr.expects(:[]=).with(:failed, true) | |
130 | txfr.expects(:[]=).with(:error, error) | |
131 | transfer.expects(:session_map).returns(s => txfr) | |
132 | transfer.process! | |
133 | end | |
134 | ||
135 | def test_uploading_a_non_existing_file_should_raise_an_understandable_error | |
136 | s = session('app1') | |
137 | error = Capistrano::Processable::SessionAssociation.on(ArgumentError.new('expected a file to upload'), s) | |
138 | transfer = Capistrano::Transfer.new(:up, "from", "to", [], :via => :scp) | |
139 | transfer.expects(:process_iteration).raises(error) | |
140 | assert_raise(ArgumentError, 'expected a file to upload') { transfer.process! } | |
141 | end | |
142 | ||
143 | private | |
144 | ||
145 | class ExceptionWithSession < ::Exception | |
146 | attr_reader :session | |
147 | ||
148 | def initialize(session) | |
149 | @session = session | |
150 | super() | |
151 | end | |
152 | end | |
153 | ||
154 | def session(host, mode=nil) | |
155 | session = stub('session', :xserver => stub('server', :host => host)) | |
156 | case mode | |
157 | when :sftp | |
158 | sftp = stub('sftp') | |
159 | session.expects(:sftp).with(false).returns(sftp) | |
160 | sftp.expects(:connect).yields(sftp).returns(sftp) | |
161 | session.stubs(:xsftp).returns(sftp) | |
162 | when :scp | |
163 | session.stubs(:scp).returns(stub('scp')) | |
164 | end | |
165 | session | |
166 | end | |
167 | end⏎ |
0 | require 'rubygems' | |
1 | require 'bundler/setup' | |
2 | ||
3 | require 'test/unit' | |
4 | require 'mocha' | |
5 | ||
6 | require 'capistrano/server_definition' | |
7 | ||
8 | module TestExtensions | |
9 | def server(host, options={}) | |
10 | Capistrano::ServerDefinition.new(host, options) | |
11 | end | |
12 | ||
13 | def namespace(fqn=nil) | |
14 | space = stub(:roles => {}, :fully_qualified_name => fqn, :default_task => nil) | |
15 | yield(space) if block_given? | |
16 | space | |
17 | end | |
18 | ||
19 | def role(space, name, *args) | |
20 | opts = args.last.is_a?(Hash) ? args.pop : {} | |
21 | space.roles[name] ||= [] | |
22 | space.roles[name].concat(args.map { |h| Capistrano::ServerDefinition.new(h, opts) }) | |
23 | end | |
24 | ||
25 | def new_task(name, namespace=@namespace, options={}, &block) | |
26 | block ||= Proc.new {} | |
27 | task = Capistrano::TaskDefinition.new(name, namespace, options, &block) | |
28 | assert_equal block, task.body | |
29 | return task | |
30 | end | |
31 | end | |
32 | ||
33 | class Test::Unit::TestCase | |
34 | include TestExtensions | |
35 | end |