Codebase list capistrano / a74e76d
Imported Upstream version 3.2.0 Sebastien Badia 10 years ago
193 changed file(s) with 5403 addition(s) and 13740 deletion(s). Raw diff Collapse all Expand all
0 *.gem
1 *.rbc
2 .bundle
3 .config
4 .yardoc
5 Gemfile.lock
6 InstalledFiles
7 _yardoc
08 coverage
1 doc
9 doc/
10 lib/bundler/man
211 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
321 *.swp
4 *.patch
5 *.gem
6 *.sh
7 .DS_Store
8 .bundle/*
9 Gemfile.lock
00 language: ruby
11 rvm:
2 - 1.8.7
2 - 2.1.0
3 - 2.0.0
4 - 1.9.3
35 - 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
-1073
CHANGELOG less more
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'
11
22 # Specify your gem's dependencies in capistrano.gemspec
33 gemspec
44
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'
118 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
-93
README.mdown less more
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"
21
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
00 #!/usr/bin/env ruby
1
2 require 'capistrano/cli'
3 Capistrano::CLI.execute
1 require 'capistrano/all'
2 Capistrano::Application.new.run
00 #!/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
00 # -*- 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'
34
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/"
513
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"]
2118
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'
4733 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 #!/usr/bin/env cap
1 include Capistrano::DSL
2 require 'capistrano/install'
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
-45
lib/capistrano/callback.rb less more
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
-85
lib/capistrano/cli/execute.rb less more
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
-125
lib/capistrano/cli/help.rb less more
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
-81
lib/capistrano/cli/help.txt less more
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
-243
lib/capistrano/cli/options.rb less more
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
-40
lib/capistrano/cli/ui.rb less more
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
-47
lib/capistrano/cli.rb less more
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
-289
lib/capistrano/command.rb less more
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
-50
lib/capistrano/configuration/actions/file_transfer.rb less more
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
-46
lib/capistrano/configuration/actions/inspect.rb less more
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
-298
lib/capistrano/configuration/actions/invocation.rb less more
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
-26
lib/capistrano/configuration/alias_task.rb less more
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
-129
lib/capistrano/configuration/callbacks.rb less more
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
-230
lib/capistrano/configuration/connections.rb less more
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
-143
lib/capistrano/configuration/execution.rb less more
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
-197
lib/capistrano/configuration/loading.rb less more
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
-216
lib/capistrano/configuration/namespaces.rb less more
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
-77
lib/capistrano/configuration/roles.rb less more
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'
03 module Capistrano
14 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)
910 end
1011
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)) }
7314 end
7415
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)
9319 end
9420
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
9924 end
10025
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 }
10828 end
10929
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 : {}
11263 end
11364 end
11465 end
+0
-127
lib/capistrano/configuration/variables.rb less more
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'
153
164 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.
205 class Configuration
21 # The logger instance defined for this configuration.
22 attr_accessor :debug, :logger, :dry_run, :preserve_roles
236
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
2915 end
3016
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
3321
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
3725
38 # Mix in the actions
39 include Actions::FileTransfer, Actions::Inspect, Actions::Invocation
26 def delete(key)
27 config.delete(key)
28 end
4029
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
43103 end
44104 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 require 'capistrano/framework'
1
2 load File.expand_path("../tasks/deploy.rake", __FILE__)
0 dotfile = Pathname.new(File.join(Dir.home, '.capfile'))
1 load dotfile if dotfile.file?
2
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
-19
lib/capistrano/errors.rb less more
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
-62
lib/capistrano/ext/multistage.rb less more
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
-5
lib/capistrano/ext/string.rb less more
0 class String
1 def compact
2 self.gsub(/\s+/, ' ')
3 end
4 end
+0
-57
lib/capistrano/extensions.rb less more
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
-8
lib/capistrano/fix_rake_deprecated_dsl.rb less more
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/framework.rake", __FILE__)
1 require 'capistrano/install'
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
-59
lib/capistrano/logger.rb less more
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
-53
lib/capistrano/processable.rb less more
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
-32
lib/capistrano/recipes/compat.rb less more
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
-60
lib/capistrano/recipes/deploy/assets.rb less more
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
-44
lib/capistrano/recipes/deploy/dependencies.rb less more
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
-54
lib/capistrano/recipes/deploy/local_dependency.rb less more
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
-117
lib/capistrano/recipes/deploy/remote_dependency.rb less more
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
-169
lib/capistrano/recipes/deploy/scm/accurev.rb less more
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
-200
lib/capistrano/recipes/deploy/scm/base.rb less more
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
-86
lib/capistrano/recipes/deploy/scm/bzr.rb less more
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
-153
lib/capistrano/recipes/deploy/scm/cvs.rb less more
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
-96
lib/capistrano/recipes/deploy/scm/darcs.rb less more
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
-291
lib/capistrano/recipes/deploy/scm/git.rb less more
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
-137
lib/capistrano/recipes/deploy/scm/mercurial.rb less more
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
-44
lib/capistrano/recipes/deploy/scm/none.rb less more
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
-147
lib/capistrano/recipes/deploy/scm/perforce.rb less more
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
-121
lib/capistrano/recipes/deploy/scm/subversion.rb less more
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
-19
lib/capistrano/recipes/deploy/scm.rb less more
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
-88
lib/capistrano/recipes/deploy/strategy/base.rb less more
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
-20
lib/capistrano/recipes/deploy/strategy/checkout.rb less more
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
-326
lib/capistrano/recipes/deploy/strategy/copy.rb less more
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
-20
lib/capistrano/recipes/deploy/strategy/export.rb less more
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
-52
lib/capistrano/recipes/deploy/strategy/remote.rb less more
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
-57
lib/capistrano/recipes/deploy/strategy/remote_cache.rb less more
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
-21
lib/capistrano/recipes/deploy/strategy/unshared_remote_cache.rb less more
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
-19
lib/capistrano/recipes/deploy/strategy.rb less more
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
-53
lib/capistrano/recipes/deploy/templates/maintenance.rhtml less more
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
-594
lib/capistrano/recipes/deploy.rb less more
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
-37
lib/capistrano/recipes/standard.rb less more
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
-53
lib/capistrano/recipes/templates/maintenance.rhtml less more
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
-102
lib/capistrano/role.rb less more
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
-56
lib/capistrano/server_definition.rb less more
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
-260
lib/capistrano/shell.rb less more
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
-95
lib/capistrano/ssh.rb less more
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
-77
lib/capistrano/task_definition.rb less more
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
-218
lib/capistrano/transfer.rb less more
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'
10 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"
152 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'
00 --- !ruby/object:Gem::Specification
11 name: capistrano
22 version: !ruby/object:Gem::Version
3 version: 2.12.0
4 prerelease:
3 version: 3.2.0
54 platform: ruby
65 authors:
7 - Jamis Buck
6 - Tom Clements
87 - Lee Hambley
98 autorequire:
109 bindir: bin
1110 cert_chain: []
12 date: 2012-04-13 00:00:00.000000000 Z
11 date: 2014-04-15 00:00:00.000000000 Z
1312 dependencies:
1413 - !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'
2220 type: :runtime
2321 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
3334 type: :runtime
3435 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'
4448 type: :runtime
4549 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'
6969 - !ruby/object:Gem::Dependency
7070 name: mocha
71 requirement: &70100200767840 !ruby/object:Gem::Requirement
72 none: false
73 requirements:
74 - - ! '>='
71 requirement: !ruby/object:Gem::Requirement
72 requirements:
73 - - '>='
7574 - !ruby/object:Gem::Version
7675 version: '0'
7776 type: :development
7877 prerelease: false
79 version_requirements: *70100200767840
78 version_requirements: !ruby/object:Gem::Requirement
79 requirements:
80 - - '>='
81 - !ruby/object:Gem::Version
82 version: '0'
8083 description: Capistrano is a utility and framework for executing commands in parallel
8184 on multiple remote machines, via SSH.
8285 email:
83 - jamis@jamisbuck.org
86 - seenmyfate@gmail.com
8487 - lee.hambley@gmail.com
8588 executables:
8689 - cap
8790 - capify
8891 extensions: []
89 extra_rdoc_files:
90 - README.mdown
92 extra_rdoc_files: []
9193 files:
9294 - .gitignore
9395 - .travis.yml
94 - CHANGELOG
96 - CHANGELOG.md
97 - CONTRIBUTING.md
9598 - Gemfile
96 - README.mdown
99 - LICENSE.txt
100 - README.md
97101 - Rakefile
98102 - bin/cap
99103 - bin/capify
100104 - 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
101116 - 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
110119 - 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
121122 - 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
164152 - 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
212187 rdoc_options: []
213188 require_paths:
214189 - lib
215190 required_ruby_version: !ruby/object:Gem::Requirement
216 none: false
217191 requirements:
218 - - ! '>='
192 - - '>='
219193 - !ruby/object:Gem::Version
220194 version: '0'
221195 required_rubygems_version: !ruby/object:Gem::Requirement
222 none: false
223196 requirements:
224 - - ! '>='
197 - - '>='
225198 - !ruby/object:Gem::Version
226199 version: '0'
227200 requirements: []
228201 rubyforge_project:
229 rubygems_version: 1.8.11
202 rubygems_version: 2.0.3
230203 signing_key:
231 specification_version: 3
204 specification_version: 4
232205 summary: Capistrano - Welcome to easy deployment with Ruby over SSH
233206 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::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
-132
test/cli/execute_test.rb less more
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
-165
test/cli/help_test.rb less more
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
-329
test/cli/options_test.rb less more
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
-28
test/cli/ui_test.rb less more
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
-17
test/cli_test.rb less more
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
-304
test/command_test.rb less more
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
-61
test/configuration/actions/file_transfer_test.rb less more
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
-65
test/configuration/actions/inspect_test.rb less more
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
-247
test/configuration/actions/invocation_test.rb less more
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
-118
test/configuration/alias_task_test.rb less more
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
-177
test/configuration/callbacks_test.rb less more
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
-420
test/configuration/connections_test.rb less more
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
-175
test/configuration/execution_test.rb less more
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
-132
test/configuration/loading_test.rb less more
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
-332
test/configuration/namespace_dsl_test.rb less more
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
-157
test/configuration/roles_test.rb less more
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
-183
test/configuration/servers_test.rb less more
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
-190
test/configuration/variables_test.rb less more
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
-77
test/configuration_test.rb less more
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
-76
test/deploy/local_dependency_test.rb less more
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
-146
test/deploy/remote_dependency_test.rb less more
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
-23
test/deploy/scm/accurev_test.rb less more
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
-55
test/deploy/scm/base_test.rb less more
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
-51
test/deploy/scm/bzr_test.rb less more
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
-37
test/deploy/scm/darcs_test.rb less more
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
-211
test/deploy/scm/git_test.rb less more
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
-134
test/deploy/scm/mercurial_test.rb less more
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
-35
test/deploy/scm/none_test.rb less more
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
-23
test/deploy/scm/perforce_test.rb less more
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
-40
test/deploy/scm/subversion_test.rb less more
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
-335
test/deploy/strategy/copy_test.rb less more
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
-69
test/extensions_test.rb less more
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
-5
test/fixtures/cli_integration.rb less more
0 role :test, "www.capistrano.test"
1
2 task :testing, :roles => :test do
3 set :testing_occurred, true
4 end
+0
-5
test/fixtures/config.rb less more
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
-3
test/fixtures/custom.rb less more
0 ConfigurationLoadingTest::MockConfig.instance(:must_exist).load do
1 ping! :custom
2 end
+0
-123
test/logger_test.rb less more
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
-25
test/recipes_test.rb less more
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
-11
test/role_test.rb less more
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
-121
test/server_definition_test.rb less more
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
-90
test/shell_test.rb less more
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
-113
test/ssh_test.rb less more
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
-117
test/task_definition_test.rb less more
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
-168
test/transfer_test.rb less more
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
-36
test/utils.rb less more
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