21 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | # Releasing
2 |
3 | ## Prerequisites
4 |
5 | * You must have commit rights to the Capistrano repository.
6 | * You must have push rights for the capistrano gem on rubygems.org.
7 |
8 | ## How to release
9 |
10 | 1. Run `bundle install` to make sure that you have all the gems necessary for testing and releasing.
11 | 2. **Ensure all tests are passing by running `rake spec` and `rake features`.**
12 | 3. Determine which would be the correct next version number according to [semver](http://semver.org/).
13 | 4. Update the version in `./lib/capistrano/version.rb`.
14 | 5. Update the version in the `./README.md` Gemfile example (`gem "capistrano", "~> X.Y"`).
15 | 6. Commit the `version.rb` and `README.md` changes in a single commit, the message should be "Release vX.Y.Z"
16 | 7. Run `rake release`; this will tag, push to GitHub, and publish to rubygems.org.
17 | 8. Update the draft release on the [GitHub releases page](https://github.com/capistrano/capistrano/releases) to point to the new tag and publish the release
18 |
--------------------------------------------------------------------------------
/docs/documentation/advanced-features/ignoring/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: Ignoring
3 | layout: default
4 | ---
5 |
6 | Files committed to version control (i.e. not in .gitignore) can still be ignored when deploying. To ignore these files or directories, simply add them to .gitattributes:
7 |
8 | ```bash
9 | config/deploy/deploy.rb export-ignore
10 | config/deploy/ export-ignore
11 | ```
12 |
13 | These files will be kept in version control but not deployed to the server.
14 |
15 | *Note:* This feature is probably unnecessary unless the root of your repository is also your web server's docroot. For example, in a Rails application, the docroot is the `public/` folder. Since all of the Capistrano configuration lives above or beside this folder, it cannot be served and is not a security risk. If the docroot is indeed at the base of the repository, consider changing that by moving the code at the repository base to a subdirectory such as public_html instead of using this feature. Note that this feature is very specific to Git and will not work on other SCMs.
16 |
--------------------------------------------------------------------------------
/lib/capistrano/immutable_task.rb:
--------------------------------------------------------------------------------
1 | module Capistrano
2 | # This module extends a Rake::Task to freeze it to prevent it from being
3 | # enhanced. This is used to prevent users from enhancing a task at the wrong
4 | # point of Capistrano's boot process, which can happen if a Capistrano plugin
5 | # is loaded in deploy.rb by mistake (instead of in the Capfile).
6 | #
7 | # Usage:
8 | #
9 | # task = Rake.application["load:defaults"]
10 | # task.invoke
11 | # task.extend(Capistrano::ImmutableTask) # prevent further modifications
12 | #
13 | module ImmutableTask
14 | def self.extended(task)
15 | task.freeze
16 | end
17 |
18 | def enhance(*args, &block)
19 | $stderr.puts <<-MESSAGE
20 | ERROR: #{name} has already been invoked and can no longer be modified.
21 | Check that you haven't loaded a Capistrano plugin in deploy.rb or a stage
22 | (e.g. deploy/production.rb) by mistake.
23 | Plugins must be loaded in the Capfile to initialize properly.
24 | MESSAGE
25 |
26 | # This will raise a frozen object error
27 | super(*args, &block)
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/capistrano/setup.rb:
--------------------------------------------------------------------------------
1 | require "capistrano/doctor"
2 | require "capistrano/immutable_task"
3 | include Capistrano::DSL
4 |
5 | namespace :load do
6 | task :defaults do
7 | load "capistrano/defaults.rb"
8 | end
9 | end
10 |
11 | require "airbrussh/capistrano"
12 | # We don't need to show the "using Airbrussh" banner announcement since
13 | # Airbrussh is now the built-in formatter. Also enable command output by
14 | # default; hiding the output might be confusing to users new to Capistrano.
15 | Airbrussh.configure do |airbrussh|
16 | airbrussh.banner = false
17 | airbrussh.command_output = true
18 | end
19 |
20 | stages.each do |stage|
21 | Rake::Task.define_task(stage) do
22 | set(:stage, stage.to_sym)
23 |
24 | invoke "load:defaults"
25 | Rake.application["load:defaults"].extend(Capistrano::ImmutableTask)
26 | env.variables.untrusted! do
27 | load deploy_config_path
28 | load stage_config_path.join("#{stage}.rb")
29 | end
30 | configure_scm
31 | I18n.locale = fetch(:locale, :en)
32 | configure_backend
33 | end
34 | end
35 |
36 | require "capistrano/dotfile"
37 |
--------------------------------------------------------------------------------
/.github/issue_template.md:
--------------------------------------------------------------------------------
1 | **Important:** GitHub issues are for feature requests or bug reports. The Capistrano team recommends you use [Stack Overflow](http://stackoverflow.com/questions/tagged/capistrano) for general questions. For more details, please see our [contribution policy](https://github.com/capistrano/capistrano/blob/master/CONTRIBUTING.md).
2 |
3 | ---
4 |
5 | ### Steps to reproduce
6 | If reasonable, you can help by creating a Capistrano skeleton example project which reproduces the issue you are seeing. You can then upload the individual files to a GitHub Gist or a GitHub project. Others can simply modify the configuration to point at a test server/repository of their own. Often times, an issue is resolved simply by making this test case.
7 |
8 | An example test case is here: https://gist.github.com/will-in-wi/527327e31af30b3eae2068e2965be05b
9 |
10 | ### Expected behavior
11 | Tell us what should happen
12 |
13 | ### Actual behavior
14 | Tell us what happens instead
15 |
16 | ### System configuration
17 | Please link to the output of `cap doctor` in a GitHub Gist.
18 |
19 | Thanks for helping improve Capistrano!
20 |
--------------------------------------------------------------------------------
/docs/documentation/advanced-features/ssh-kit/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: Remote commands with SSH Kit
3 | layout: default
4 | ---
5 |
6 | Capistrano executes commands on remote servers using [**SSHKit**](https://github.com/capistrano/sshkit).
7 |
8 | An example setting a working directory, user and environment variable:
9 |
10 | ```ruby
11 | on roles(:app), in: :sequence, wait: 5 do
12 | within "/opt/sites/example.com" do
13 | # commands in this block execute in the
14 | # directory: /opt/sites/example.com
15 | as :deploy do
16 | # commands in this block execute as the "deploy" user.
17 | with rails_env: :production do
18 | # commands in this block execute with the environment
19 | # variable RAILS_ENV=production
20 | rake "assets:precompile"
21 | runner "S3::Sync.notify"
22 | end
23 | end
24 | end
25 | end
26 | ```
27 |
28 | For more examples, see the EXAMPLES.md file in the [**SSHKit**](https://github.com/capistrano/sshkit) project:
29 |
30 | [https://github.com/capistrano/sshkit/blob/master/EXAMPLES.md](https://github.com/capistrano/sshkit/blob/master/EXAMPLES.md)
31 |
--------------------------------------------------------------------------------
/features/configuration.feature:
--------------------------------------------------------------------------------
1 | Feature: The path to the configuration can be changed, removing the need to
2 | follow Ruby/Rails conventions
3 |
4 | Background:
5 | Given a test app with the default configuration
6 | And servers with the roles app and web
7 |
8 | Scenario: Deploying with configuration in default location
9 | When I run "cap test"
10 | Then the task is successful
11 |
12 | Scenario: Deploying with configuration in a custom location
13 | But the configuration is in a custom location
14 | When I run "cap test"
15 | Then the task is successful
16 |
17 | Scenario: Show install task with configuration in default location
18 | When I run "cap -T"
19 | Then the task is successful
20 | And contains "install" in the output
21 |
22 | Scenario: Hide install task with configuration in a custom location
23 | And config stage file has line "desc 'Special Task'"
24 | And config stage file has line "task :special_stage_task"
25 | But the configuration is in a custom location
26 | When I run "cap -T"
27 | Then the task is successful
28 | And doesn't contain "special_stage_task" in the output
29 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License (MIT)
2 |
3 | Copyright (c) 2012-2020 Tom Clements, Lee Hambley
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/lib/capistrano/dsl/env.rb:
--------------------------------------------------------------------------------
1 | require "forwardable"
2 |
3 | module Capistrano
4 | module DSL
5 | module Env
6 | extend Forwardable
7 | def_delegators :env,
8 | :configure_backend, :fetch, :set, :set_if_empty, :delete,
9 | :ask, :role, :server, :primary, :validate, :append,
10 | :remove, :dry_run?, :install_plugin, :any?, :is_question?,
11 | :configure_scm, :scm_plugin_installed?
12 |
13 | def roles(*names)
14 | env.roles_for(names.flatten)
15 | end
16 |
17 | def role_properties(*names, &block)
18 | env.role_properties_for(names, &block)
19 | end
20 |
21 | def release_roles(*names)
22 | if names.last.is_a? Hash
23 | names.last[:exclude] = :no_release
24 | else
25 | names << { exclude: :no_release }
26 | end
27 | roles(*names)
28 | end
29 |
30 | def env
31 | Configuration.env
32 | end
33 |
34 | def release_timestamp
35 | env.timestamp.strftime("%Y%m%d%H%M%S")
36 | end
37 |
38 | def asset_timestamp
39 | env.timestamp.strftime("%Y%m%d%H%M.%S")
40 | end
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/lib/capistrano/templates/Capfile:
--------------------------------------------------------------------------------
1 | # Load DSL and set up stages
2 | require "capistrano/setup"
3 |
4 | # Include default deployment tasks
5 | require "capistrano/deploy"
6 |
7 | # Load the SCM plugin appropriate to your project:
8 | #
9 | # require "capistrano/scm/hg"
10 | # install_plugin Capistrano::SCM::Hg
11 | # or
12 | # require "capistrano/scm/svn"
13 | # install_plugin Capistrano::SCM::Svn
14 | # or
15 | require "capistrano/scm/git"
16 | install_plugin Capistrano::SCM::Git
17 |
18 | # Include tasks from other gems included in your Gemfile
19 | #
20 | # For documentation on these, see for example:
21 | #
22 | # https://github.com/capistrano/rvm
23 | # https://github.com/capistrano/rbenv
24 | # https://github.com/capistrano/chruby
25 | # https://github.com/capistrano/bundler
26 | # https://github.com/capistrano/rails
27 | # https://github.com/capistrano/passenger
28 | #
29 | # require "capistrano/rvm"
30 | # require "capistrano/rbenv"
31 | # require "capistrano/chruby"
32 | # require "capistrano/bundler"
33 | # require "capistrano/rails/assets"
34 | # require "capistrano/rails/migrations"
35 | # require "capistrano/passenger"
36 |
37 | # Load custom tasks from `lib/capistrano/tasks` if you have any defined
38 | Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r }
39 |
--------------------------------------------------------------------------------
/docs/documentation/getting-started/before-after/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: Before / After Hooks
3 | layout: default
4 | ---
5 |
6 | Where calling on the same task name, executed in order of inclusion
7 |
8 | ```ruby
9 | # call an existing task
10 | before :starting, :ensure_user
11 |
12 | after :finishing, :notify
13 |
14 |
15 | # or define in block
16 | namespace :deploy do
17 | before :starting, :ensure_user do
18 | #
19 | end
20 |
21 | after :finishing, :notify do
22 | #
23 | end
24 | end
25 | ```
26 |
27 | If it makes sense for your use case (often, that means *generating a file*)
28 | the Rake prerequisite mechanism can be used:
29 |
30 | ```ruby
31 | desc "Create Important File"
32 | file 'important.txt' do |t|
33 | sh "touch #{t.name}"
34 | end
35 | desc "Upload Important File"
36 | task :upload => 'important.txt' do |t|
37 | on roles(:all) do
38 | upload!(t.prerequisites.first, '/tmp')
39 | end
40 | end
41 | ```
42 |
43 | The final way to call out to other tasks is to simply `invoke()` them:
44 |
45 | ```ruby
46 | namespace :example do
47 | task :one do
48 | on roles(:all) { info "One" }
49 | end
50 | task :two do
51 | invoke "example:one"
52 | on roles(:all) { info "Two" }
53 | end
54 | end
55 | ```
56 |
57 | This method is widely used.
58 |
--------------------------------------------------------------------------------
/docs/documentation/advanced-features/console/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: Console
3 | layout: default
4 | ---
5 |
6 | **Note:** Here be dragons. The console is very immature, but it's much more
7 | cleanly architected than previous incarnations and it'll only get better from
8 | here on in.
9 |
10 | Execute arbitrary remote commands, to use this simply add
11 | `require 'capistrano/console'` which will add the necessary tasks to your
12 | environment:
13 |
14 | ```bash
15 | $ bundle exec cap staging console
16 | ```
17 |
18 | Then, after setting up the server connections, this is how that might look:
19 |
20 | ```bash
21 | $ bundle exec cap production console
22 | capistrano console - enter command to execute on production
23 | production> uptime
24 | INFO [94db8027] Running /usr/bin/env uptime on leehambley@example.com:22
25 | DEBUG [94db8027] Command: /usr/bin/env uptime
26 | DEBUG [94db8027] 17:11:17 up 50 days, 22:31, 1 user, load average: 0.02, 0.02, 0.05
27 | INFO [94db8027] Finished in 0.435 seconds command successful.
28 | production> who
29 | INFO [9ce34809] Running /usr/bin/env who on leehambley@example.com:22
30 | DEBUG [9ce34809] Command: /usr/bin/env who
31 | DEBUG [9ce34809] leehambley pts/0 2013-06-13 17:11 (port-11262.pppoe.wtnet.de)
32 | INFO [9ce34809] Finished in 0.420 seconds command successful.
33 | ```
34 |
--------------------------------------------------------------------------------
/docs/_includes/metrics.html:
--------------------------------------------------------------------------------
1 |
14 |
15 |
21 |
22 |
31 |
--------------------------------------------------------------------------------
/docs/documentation/advanced-features/ptys/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: PTYs
3 | layout: default
4 | ---
5 |
6 | There is a configuration option which asks the backend driver to ask the
7 | remote host to assign the connection a *pty*. A *pty* is a pseudo-terminal,
8 | which in effect means *tell the backend that this is an __interactive__
9 | session*. This is normally a bad idea.
10 |
11 | Most of the differences are best explained by [this
12 | page](https://github.com/sstephenson/rbenv/wiki/Unix-shell-initialization)
13 | from the author of *rbenv*.
14 |
15 | **When Capistrano makes a connection it is a *non-login*, *non-interactive*
16 | shell. This was not an accident!**
17 |
18 | It's often used as a band aid to cure issues related to RVM and rbenv not
19 | loading login and shell initialisation scripts. In these scenarios RVM and
20 | rbenv are the tools at fault, or at least they are being used incorrectly.
21 |
22 | Whilst, especially in the case of language runtimes (Ruby, Node, Python and
23 | friends in particular) there is a temptation to run multiple versions in
24 | parallel on a single server and to switch between them using environmental
25 | variables, this is an anti-pattern, and symptomatic of bad design (e.g. you're
26 | testing a second version of Ruby in production because your company lacks the
27 | infrastructure to test this in a staging environment).
28 |
--------------------------------------------------------------------------------
/docs/documentation/faq/how-can-i-set-capistrano-configuration-paths/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: How can I set Capistrano configuration paths?
3 | layout: default
4 | ---
5 |
6 | Capistrano `config` and `tasks` paths can be explicitly defined, like so:
7 |
8 | Capfile
9 |
10 | ```ruby
11 | # default deploy_config_path is 'config/deploy.rb'
12 | set :deploy_config_path, 'cap/deploy.rb'
13 | # default stage_config_path is 'config/deploy'
14 | set :stage_config_path, 'cap/stages'
15 |
16 | # previous variables MUST be set before 'capistrano/setup'
17 | require 'capistrano/setup'
18 |
19 | # default tasks path is `lib/capistrano/tasks/*.rake`
20 | # (note that you can also change the file extensions)
21 | Dir.glob('cap/tasks/*.rb').each { |r| import r }
22 | ```
23 |
24 | Here is the corresponding capistrano configuration structure:
25 |
26 | ```bash
27 | ├── Capfile
28 | └── cap
29 | ├── stages
30 | │ ├── production.rb
31 | │ └── staging.rb
32 | ├── tasks
33 | │ └── custom_tasks.rb
34 | └── deploy.rb
35 | ```
36 |
37 |
38 | Be aware that you will have to provide an absolute path, if you want your "deploy_config_path" to be "capistrano/deploy.rb".
39 | See this issue for more explanations and how to get an absolute path in Ruby.
40 |
41 |
--------------------------------------------------------------------------------
/lib/capistrano/defaults.rb:
--------------------------------------------------------------------------------
1 | validate :application do |_key, value|
2 | changed_value = value.gsub(/[^A-Z0-9\.\-]/i, "_")
3 | if value != changed_value
4 | warn %Q(The :application value "#{value}" is invalid!)
5 | warn "Use only letters, numbers, hyphens, dots, and underscores. For example:"
6 | warn " set :application, '#{changed_value}'"
7 | raise Capistrano::ValidationError
8 | end
9 | end
10 |
11 | %i(git_strategy hg_strategy svn_strategy).each do |strategy|
12 | validate(strategy) do |key, _value|
13 | warn(
14 | "[Deprecation Warning] #{key} is deprecated and will be removed in "\
15 | "Capistrano 3.7.0.\n"\
16 | "https://github.com/capistrano/capistrano/blob/master/UPGRADING-3.7.md"
17 | )
18 | end
19 | end
20 |
21 | # We use a special :_default_git value so that SCMResolver can tell whether the
22 | # default has been replaced by the user via `set`.
23 | set_if_empty :scm, Capistrano::Configuration::SCMResolver::DEFAULT_GIT
24 | set_if_empty :branch, "master"
25 | set_if_empty :deploy_to, -> { "/var/www/#{fetch(:application)}" }
26 | set_if_empty :tmp_dir, "/tmp"
27 |
28 | set_if_empty :default_env, {}
29 | set_if_empty :keep_releases, 5
30 |
31 | set_if_empty :format, :airbrussh
32 | set_if_empty :log_level, :debug
33 |
34 | set_if_empty :pty, false
35 |
36 | set_if_empty :local_user, -> { ENV["USER"] || ENV["LOGNAME"] || ENV["USERNAME"] }
37 |
--------------------------------------------------------------------------------
/lib/capistrano/doctor/gems_doctor.rb:
--------------------------------------------------------------------------------
1 | require "capistrano/doctor/output_helpers"
2 |
3 | module Capistrano
4 | module Doctor
5 | # Prints table of all Capistrano-related gems and their version numbers. If
6 | # there is a newer version of a gem available, call attention to it.
7 | class GemsDoctor
8 | include Capistrano::Doctor::OutputHelpers
9 |
10 | def call
11 | title("Gems")
12 | table(all_gem_names) do |gem, row|
13 | row.yellow if update_available?(gem)
14 | row << gem
15 | row << installed_gem_version(gem)
16 | row << "(update available)" if update_available?(gem)
17 | end
18 | end
19 |
20 | private
21 |
22 | def installed_gem_version(gem_name)
23 | Gem.loaded_specs[gem_name].version
24 | end
25 |
26 | def update_available?(gem_name)
27 | latest = Gem.latest_version_for(gem_name)
28 | return false if latest.nil?
29 | latest > installed_gem_version(gem_name)
30 | end
31 |
32 | def all_gem_names
33 | core_gem_names + plugin_gem_names
34 | end
35 |
36 | def core_gem_names
37 | %w(capistrano airbrussh rake sshkit net-ssh) & Gem.loaded_specs.keys
38 | end
39 |
40 | def plugin_gem_names
41 | (Gem.loaded_specs.keys - ["capistrano"]).grep(/capistrano/).sort
42 | end
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/capistrano/scm/tasks/hg.rake:
--------------------------------------------------------------------------------
1 | # TODO: this is nearly identical to git.rake. DRY up?
2 |
3 | # This trick lets us access the Hg plugin within `on` blocks.
4 | hg_plugin = self
5 |
6 | namespace :hg do
7 | desc "Check that the repo is reachable"
8 | task :check do
9 | on release_roles :all do
10 | hg_plugin.check_repo_is_reachable
11 | end
12 | end
13 |
14 | desc "Clone the repo to the cache"
15 | task :clone do
16 | on release_roles :all do
17 | if hg_plugin.repo_mirror_exists?
18 | info t(:mirror_exists, at: repo_path)
19 | else
20 | within deploy_path do
21 | hg_plugin.clone_repo
22 | end
23 | end
24 | end
25 | end
26 |
27 | desc "Pull changes from the remote repo"
28 | task update: :'hg:clone' do
29 | on release_roles :all do
30 | within repo_path do
31 | hg_plugin.update_mirror
32 | end
33 | end
34 | end
35 |
36 | desc "Copy repo to releases"
37 | task create_release: :'hg:update' do
38 | on release_roles :all do
39 | within repo_path do
40 | hg_plugin.archive_to_release_path
41 | end
42 | end
43 | end
44 |
45 | desc "Determine the revision that will be deployed"
46 | task :set_current_revision do
47 | on release_roles :all do
48 | within repo_path do
49 | set :current_revision, hg_plugin.fetch_revision
50 | end
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/capistrano/scm/tasks/svn.rake:
--------------------------------------------------------------------------------
1 | # TODO: this is nearly identical to git.rake. DRY up?
2 |
3 | # This trick lets us access the Svn plugin within `on` blocks.
4 | svn_plugin = self
5 |
6 | namespace :svn do
7 | desc "Check that the repo is reachable"
8 | task :check do
9 | on release_roles :all do
10 | svn_plugin.check_repo_is_reachable
11 | end
12 | end
13 |
14 | desc "Clone the repo to the cache"
15 | task :clone do
16 | on release_roles :all do
17 | if svn_plugin.repo_mirror_exists?
18 | info t(:mirror_exists, at: repo_path)
19 | else
20 | within deploy_path do
21 | svn_plugin.clone_repo
22 | end
23 | end
24 | end
25 | end
26 |
27 | desc "Pull changes from the remote repo"
28 | task update: :'svn:clone' do
29 | on release_roles :all do
30 | within repo_path do
31 | svn_plugin.update_mirror
32 | end
33 | end
34 | end
35 |
36 | desc "Copy repo to releases"
37 | task create_release: :'svn:update' do
38 | on release_roles :all do
39 | within repo_path do
40 | svn_plugin.archive_to_release_path
41 | end
42 | end
43 | end
44 |
45 | desc "Determine the revision that will be deployed"
46 | task :set_current_revision do
47 | on release_roles :all do
48 | within repo_path do
49 | set :current_revision, svn_plugin.fetch_revision
50 | end
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/docs/assets/js/foundation/foundation.alerts.js:
--------------------------------------------------------------------------------
1 | /*jslint unparam: true, browser: true, indent: 2 */
2 |
3 | ;(function ($, window, document, undefined) {
4 | 'use strict';
5 |
6 | Foundation.libs.alerts = {
7 | name : 'alerts',
8 |
9 | version : '4.2.2',
10 |
11 | settings : {
12 | speed: 300, // fade out speed
13 | callback: function (){}
14 | },
15 |
16 | init : function (scope, method, options) {
17 | this.scope = scope || this.scope;
18 |
19 | if (typeof method === 'object') {
20 | $.extend(true, this.settings, method);
21 | }
22 |
23 | if (typeof method !== 'string') {
24 | if (!this.settings.init) { this.events(); }
25 |
26 | return this.settings.init;
27 | } else {
28 | return this[method].call(this, options);
29 | }
30 | },
31 |
32 | events : function () {
33 | var self = this;
34 |
35 | $(this.scope).on('click.fndtn.alerts', '[data-alert] a.close', function (e) {
36 | e.preventDefault();
37 | $(this).closest("[data-alert]").fadeOut(self.speed, function () {
38 | $(this).remove();
39 | self.settings.callback();
40 | });
41 | });
42 |
43 | this.settings.init = true;
44 | },
45 |
46 | off : function () {
47 | $(this.scope).off('.fndtn.alerts');
48 | },
49 |
50 | reflow : function () {}
51 | };
52 | }(Foundation.zj, this, this.document));
53 |
--------------------------------------------------------------------------------
/spec/lib/capistrano/doctor/environment_doctor_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "capistrano/doctor/environment_doctor"
3 |
4 | module Capistrano
5 | module Doctor
6 | describe EnvironmentDoctor do
7 | let(:doc) { EnvironmentDoctor.new }
8 |
9 | it "prints using 4-space indentation" do
10 | expect { doc.call }.to output(/^ {4}/).to_stdout
11 | end
12 |
13 | it "prints the Ruby version" do
14 | expect { doc.call }.to\
15 | output(/#{Regexp.quote(RUBY_DESCRIPTION)}/).to_stdout
16 | end
17 |
18 | it "prints the Rubygems version" do
19 | expect { doc.call }.to output(/#{Regexp.quote(Gem::VERSION)}/).to_stdout
20 | end
21 |
22 | describe "Rake" do
23 | before do
24 | load File.expand_path("../../../../../lib/capistrano/doctor.rb",
25 | __FILE__)
26 | end
27 |
28 | after do
29 | Rake::Task.clear
30 | end
31 |
32 | it "has an doctor:environment task that calls EnvironmentDoctor", capture_io: true do
33 | EnvironmentDoctor.any_instance.expects(:call)
34 | Rake::Task["doctor:environment"].invoke
35 | end
36 |
37 | it "has a doctor task that depends on doctor:environment" do
38 | expect(Rake::Task["doctor"].prerequisites).to \
39 | include("doctor:environment")
40 | end
41 | end
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/features/support/docker_gateway.rb:
--------------------------------------------------------------------------------
1 | # Ensure Docker container is completely stopped when Ruby exits.
2 | at_exit do
3 | DockerGateway.new.stop
4 | end
5 |
6 | # Manages the Docker-based SSH server that is declared in docker-compose.yml.
7 | class DockerGateway
8 | def initialize(log_proc=$stderr.method(:puts))
9 | @log_proc = log_proc
10 | end
11 |
12 | def start
13 | run_compose_command("up -d")
14 | end
15 |
16 | def stop
17 | run_compose_command("down")
18 | end
19 |
20 | def run_shell_command(command)
21 | run_compose_command("exec ssh_server /bin/bash -c #{command.shellescape}")
22 | end
23 |
24 | private
25 |
26 | def run_compose_command(command)
27 | log "[docker compose] #{command}"
28 | stdout, stderr, status = Open3.popen3("docker compose #{command}") do |stdin, stdout, stderr, wait_threads|
29 | stdin << ""
30 | stdin.close
31 | out = Thread.new { read_lines(stdout, &$stdout.method(:puts)) }
32 | err = Thread.new { stderr.read }
33 | [out.value, err.value.to_s, wait_threads.value]
34 | end
35 |
36 | (stdout + stderr).each_line { |line| log "[docker compose] #{line}" }
37 |
38 | [stdout, stderr, status]
39 | end
40 |
41 | def read_lines(io)
42 | buffer = + ""
43 | while (line = io.gets)
44 | buffer << line
45 | yield line
46 | end
47 | buffer
48 | end
49 |
50 | def log(message)
51 | @log_proc.call(message)
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/docs/documentation/getting-started/tasks/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: Tasks
3 | layout: default
4 | ---
5 |
6 | ```ruby
7 | server 'example.com', roles: [:web, :app]
8 | server 'example.org', roles: [:db, :workers]
9 | desc "Report Uptimes"
10 | task :uptime do
11 | on roles(:all) do |host|
12 | execute :any_command, "with args", :here, "and here"
13 | info "Host #{host} (#{host.roles.to_a.join(', ')}):\t#{capture(:uptime)}"
14 | end
15 | end
16 | ```
17 |
18 | **Note**:
19 |
20 | **tl;dr**: `execute(:bundle, :install)` and `execute('bundle install')` don't behave identically!
21 |
22 | `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.
23 |
24 | 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.
25 |
--------------------------------------------------------------------------------
/docs/_includes/footer.html:
--------------------------------------------------------------------------------
1 |
32 |
--------------------------------------------------------------------------------
/lib/capistrano/tasks/install.rake:
--------------------------------------------------------------------------------
1 | require "erb"
2 | require "pathname"
3 | desc "Install Capistrano, cap install STAGES=staging,production"
4 | task :install do
5 | envs = ENV["STAGES"] || "staging,production"
6 |
7 | tasks_dir = Pathname.new("lib/capistrano/tasks")
8 | config_dir = Pathname.new("config")
9 | deploy_dir = config_dir.join("deploy")
10 |
11 | deploy_rb = File.expand_path("../../templates/deploy.rb.erb", __FILE__)
12 | stage_rb = File.expand_path("../../templates/stage.rb.erb", __FILE__)
13 | capfile = File.expand_path("../../templates/Capfile", __FILE__)
14 |
15 | mkdir_p deploy_dir
16 |
17 | entries = [{ template: deploy_rb, file: config_dir.join("deploy.rb") }]
18 | entries += envs.split(",").map { |stage| { template: stage_rb, file: deploy_dir.join("#{stage}.rb") } }
19 |
20 | entries.each do |entry|
21 | if File.exist?(entry[:file])
22 | warn "[skip] #{entry[:file]} already exists"
23 | else
24 | File.open(entry[:file], "w+") do |f|
25 | f.write(ERB.new(File.read(entry[:template])).result(binding))
26 | puts I18n.t(:written_file, scope: :capistrano, file: entry[:file])
27 | end
28 | end
29 | end
30 |
31 | mkdir_p tasks_dir
32 |
33 | if File.exist?("Capfile")
34 | warn "[skip] Capfile already exists"
35 | else
36 | FileUtils.cp(capfile, "Capfile")
37 | puts I18n.t(:written_file, scope: :capistrano, file: "Capfile")
38 | end
39 |
40 | puts I18n.t :capified, scope: :capistrano
41 | end
42 |
--------------------------------------------------------------------------------
/lib/capistrano/tasks/framework.rake:
--------------------------------------------------------------------------------
1 | namespace :deploy do
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
68 |
--------------------------------------------------------------------------------
/lib/capistrano/templates/deploy.rb.erb:
--------------------------------------------------------------------------------
1 | # config valid for current version and patch releases of Capistrano
2 | lock "~> <%= Capistrano::VERSION %>"
3 |
4 | set :application, "my_app_name"
5 | set :repo_url, "git@example.com:me/my_repo.git"
6 |
7 | # Default branch is :master
8 | # ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp
9 |
10 | # Default deploy_to directory is /var/www/my_app_name
11 | # set :deploy_to, "/var/www/my_app_name"
12 |
13 | # Default value for :format is :airbrussh.
14 | # set :format, :airbrussh
15 |
16 | # You can configure the Airbrussh format using :format_options.
17 | # These are the defaults.
18 | # set :format_options, command_output: true, log_file: "log/capistrano.log", color: :auto, truncate: :auto
19 |
20 | # Default value for :pty is false
21 | # set :pty, true
22 |
23 | # Default value for :linked_files is []
24 | # append :linked_files, "config/database.yml", 'config/master.key'
25 |
26 | # Default value for linked_dirs is []
27 | # append :linked_dirs, "log", "tmp/pids", "tmp/cache", "tmp/sockets", "public/system", "vendor", "storage"
28 |
29 | # Default value for default_env is {}
30 | # set :default_env, { path: "/opt/ruby/bin:$PATH" }
31 |
32 | # Default value for local_user is ENV['USER']
33 | # set :local_user, -> { `git config user.name`.chomp }
34 |
35 | # Default value for keep_releases is 5
36 | # set :keep_releases, 5
37 |
38 | # Uncomment the following to require manually verifying the host key before first deploy.
39 | # set :ssh_options, verify_host_key: :secure
40 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | DisplayCopNames: true
3 | DisplayStyleGuide: true
4 | TargetRubyVersion: 2.0
5 |
6 | Lint/AmbiguousBlockAssociation:
7 | Enabled:
8 | false
9 | Metrics/BlockLength:
10 | Exclude:
11 | - "*.gemspec"
12 | - "spec/**/*"
13 | - "lib/**/*.rake"
14 | Style/BarePercentLiterals:
15 | EnforcedStyle: percent_q
16 | Style/BracesAroundHashParameters:
17 | Exclude:
18 | - spec/integration/dsl_spec.rb
19 | Style/ClassAndModuleChildren:
20 | Enabled: false
21 | Style/DoubleNegation:
22 | Enabled: false
23 | Style/IndentHeredoc:
24 | Enabled: false
25 | Style/SpaceAroundEqualsInParameterDefault:
26 | EnforcedStyle: no_space
27 | Style/StringLiterals:
28 | EnforcedStyle: double_quotes
29 | Style/TrivialAccessors:
30 | AllowPredicates: true
31 | Style/PercentLiteralDelimiters:
32 | Enabled: false
33 | Style/SingleLineBlockParams:
34 | Enabled: false
35 | Style/ModuleFunction:
36 | Enabled: false
37 |
38 | # Enable someday
39 | Style/Documentation:
40 | Enabled: false
41 |
42 | # Needs refactors
43 | Metrics/PerceivedComplexity:
44 | Enabled: false
45 | Metrics/CyclomaticComplexity:
46 | Enabled: false
47 | Metrics/MethodLength:
48 | Enabled: false
49 | Style/PredicateName:
50 | Enabled: false
51 | Metrics/LineLength:
52 | Enabled: false
53 | Metrics/AbcSize:
54 | Enabled: false
55 | Style/PerlBackrefs:
56 | Enabled: false
57 | Metrics/ClassLength:
58 | Enabled: false
59 | Metrics/ModuleLength:
60 | Enabled: false
61 | Style/AccessorMethodName:
62 | Enabled: false
63 |
--------------------------------------------------------------------------------
/capistrano.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | lib = File.expand_path("../lib", __FILE__)
4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5 | require "capistrano/version"
6 |
7 | Gem::Specification.new do |gem|
8 | gem.name = "capistrano"
9 | gem.version = Capistrano::VERSION
10 | gem.authors = ["Tom Clements", "Lee Hambley"]
11 | gem.email = ["seenmyfate@gmail.com", "lee.hambley@gmail.com"]
12 | gem.description = "Capistrano is a utility and framework for executing commands in parallel on multiple remote machines, via SSH."
13 | gem.summary = "Capistrano - Welcome to easy deployment with Ruby over SSH"
14 | gem.homepage = "https://capistranorb.com/"
15 | gem.metadata = {
16 | "bug_tracker_uri" => "https://github.com/capistrano/capistrano/issues",
17 | "changelog_uri" => "https://github.com/capistrano/capistrano/releases",
18 | "source_code_uri" => "https://github.com/capistrano/capistrano",
19 | "homepage_uri" => "https://capistranorb.com/",
20 | "documentation_uri" => "https://capistranorb.com/"
21 | }
22 | gem.files = `git ls-files -z`.split("\x0").reject { |f| f =~ /^docs/ }
23 | gem.executables = %w(cap capify)
24 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
25 | gem.require_paths = ["lib"]
26 |
27 | gem.licenses = ["MIT"]
28 |
29 | gem.required_ruby_version = ">= 2.0"
30 | gem.add_dependency "airbrussh", ">= 1.0.0"
31 | gem.add_dependency "i18n"
32 | gem.add_dependency "rake", ">= 10.0.0"
33 | gem.add_dependency "sshkit", ">= 1.9.0"
34 | end
35 |
--------------------------------------------------------------------------------
/spec/lib/capistrano/doctor/output_helpers_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "capistrano/doctor/output_helpers"
3 |
4 | module Capistrano
5 | module Doctor
6 | describe OutputHelpers do
7 | include OutputHelpers
8 |
9 | # Force color for the purpose of these tests
10 | before { ENV.stubs(:[]).with("SSHKIT_COLOR").returns("1") }
11 |
12 | it "prints titles in blue with newlines and without indentation" do
13 | expect { title("Hello!") }.to\
14 | output("\e[0;34;49m\nHello!\n\e[0m\n").to_stdout
15 | end
16 |
17 | it "prints warnings in yellow with 4-space indentation" do
18 | expect { warning("Yikes!") }.to\
19 | output(" \e[0;33;49mYikes!\e[0m\n").to_stdout
20 | end
21 |
22 | it "overrides puts to indent 4 spaces per line" do
23 | expect { puts("one\ntwo") }.to output(" one\n two\n").to_stdout
24 | end
25 |
26 | it "formats tables with indent, aligned columns and per-row color" do
27 | data = [
28 | ["one", ".", "1"],
29 | ["two", "..", "2"],
30 | ["three", "...", "3"]
31 | ]
32 | block = proc do |record, row|
33 | row.yellow if record.first == "two"
34 | row << record[0]
35 | row << record[1]
36 | row << record[2]
37 | end
38 | expected_output = <<-OUT
39 | one . 1
40 | \e[0;33;49mtwo .. 2\e[0m
41 | three ... 3
42 | OUT
43 | expect { table(data, &block) }.to output(expected_output).to_stdout
44 | end
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | # Specify your gem's dependencies in capistrano.gemspec
4 | gemspec
5 |
6 | gem "mocha"
7 | gem "rspec"
8 | gem "rspec-core", "~> 3.4.4"
9 |
10 | group :cucumber do
11 | # Latest versions of cucumber don't support Ruby < 2.1
12 | # rubocop:disable Bundler/DuplicatedGem
13 | if Gem::Requirement.new("< 2.1").satisfied_by?(Gem::Version.new(RUBY_VERSION))
14 | gem "cucumber", "< 3.0.1"
15 | else
16 | gem "cucumber"
17 | end
18 | # rubocop:enable Bundler/DuplicatedGem
19 | end
20 |
21 | # Latest versions of net-ssh don't support Ruby < 2.2.6
22 | if Gem::Requirement.new("< 2.2.6").satisfied_by?(Gem::Version.new(RUBY_VERSION))
23 | gem "net-ssh", "< 5.0.0"
24 | end
25 |
26 | # Latest versions of public_suffix don't support Ruby < 2.1
27 | if Gem::Requirement.new("< 2.1").satisfied_by?(Gem::Version.new(RUBY_VERSION))
28 | gem "public_suffix", "< 3.0.0"
29 | end
30 |
31 | # Latest versions of i18n don't support Ruby < 2.4
32 | if Gem::Requirement.new("< 2.4").satisfied_by?(Gem::Version.new(RUBY_VERSION))
33 | gem "i18n", "< 1.3.0"
34 | end
35 |
36 | # Latest versions of rake don't support Ruby < 2.2
37 | if Gem::Requirement.new("< 2.2").satisfied_by?(Gem::Version.new(RUBY_VERSION))
38 | gem "rake", "< 13.0.0"
39 | end
40 |
41 | # We only run rubocop and its dependencies on a new-ish ruby; no need to install them otherwise
42 | if Gem::Requirement.new("> 2.4").satisfied_by?(Gem::Version.new(RUBY_VERSION))
43 | gem "base64"
44 | gem "psych", "< 4" # Ensures rubocop works on Ruby 3.1
45 | gem "racc"
46 | gem "rubocop", "0.48.1"
47 | end
48 |
--------------------------------------------------------------------------------
/lib/capistrano/scm/hg.rb:
--------------------------------------------------------------------------------
1 | require "capistrano/scm/plugin"
2 | require "securerandom"
3 |
4 | class Capistrano::SCM::Hg < Capistrano::SCM::Plugin
5 | def register_hooks
6 | after "deploy:new_release_path", "hg:create_release"
7 | before "deploy:check", "hg:check"
8 | before "deploy:set_current_revision", "hg:set_current_revision"
9 | end
10 |
11 | def define_tasks
12 | eval_rakefile File.expand_path("../tasks/hg.rake", __FILE__)
13 | end
14 |
15 | def hg(*args)
16 | args.unshift(:hg)
17 | backend.execute(*args)
18 | end
19 |
20 | def repo_mirror_exists?
21 | backend.test " [ -d #{repo_path}/.hg ] "
22 | end
23 |
24 | def check_repo_is_reachable
25 | hg "id", repo_url
26 | end
27 |
28 | def clone_repo
29 | hg "clone", "--noupdate", repo_url, repo_path.to_s
30 | end
31 |
32 | def update_mirror
33 | hg "pull"
34 | end
35 |
36 | def archive_to_release_path
37 | if (tree = fetch(:repo_tree))
38 | tree = tree.slice %r#^/?(.*?)/?$#, 1
39 | components = tree.split("/").size
40 | temp_tar = "#{fetch(:tmp_dir)}/#{SecureRandom.hex(10)}.tar"
41 |
42 | hg "archive -p . -I", tree, "--rev", fetch(:branch), temp_tar
43 |
44 | backend.execute :mkdir, "-p", release_path
45 | backend.execute :tar, "-x --strip-components #{components} -f", temp_tar, "-C", release_path
46 | backend.execute :rm, temp_tar
47 | else
48 | hg "archive", release_path, "--rev", fetch(:branch)
49 | end
50 | end
51 |
52 | def fetch_revision
53 | backend.capture(:hg, "log --rev #{fetch(:branch)} --template \"{node}\n\"")
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/docs/documentation/tasks/rails/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: Custom Rails Tasks
3 | layout: default
4 | ---
5 |
6 | Many of these tasks probably require [Capistrano::Rails](https://github.com/capistrano/rails).
7 |
8 | ### Run arbitrary rake tasks from environment variables
9 |
10 | From [Capistrano/Rails PR #209](https://github.com/capistrano/rails/pull/209)
11 |
12 | ```ruby
13 | namespace :deploy do
14 | desc 'Runs any rake task, cap deploy:rake task=db:rollback'
15 | task rake: [:set_rails_env] do
16 | on release_roles([:db]) do
17 | within release_path do
18 | with rails_env: fetch(:rails_env) do
19 | execute :rake, ENV['task']
20 | end
21 | end
22 | end
23 | end
24 | end
25 | ```
26 |
27 | Passes in the rake task to be run via an environment variable. Also a simple example of running a rake task on the server.
28 |
29 | ```bash
30 | bundle exec cap production deploy:rake task=db:seed
31 | ```
32 |
33 |
34 | ### Conditional migrations
35 |
36 | Arising from [Capistrano/Rails issue #199](https://github.com/capistrano/rails/issues/199)
37 |
38 | A frequent issue on deploy are slow migrations which involve downtime. In this case, you often want to run the migrations conditionally, where the main deploy doesn't run them, but you can do so manually at a better point. To do so, you could put the following in your `Capfile`:
39 |
40 | ```ruby
41 | require 'capistrano/rails/migrations' if ENV['RUN_MIGRATIONS']
42 | ```
43 |
44 | Now the migrations do not run by default, but they will run with the following command:
45 |
46 | ```bash
47 | RUN_MIGRATIONS=1 bundle exec cap production deploy:migrate
48 | ```
49 |
--------------------------------------------------------------------------------
/spec/lib/capistrano/configuration/scm_resolver_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "capistrano/scm"
3 |
4 | module Capistrano
5 | class Configuration
6 | describe SCMResolver do
7 | include Capistrano::DSL
8 |
9 | let(:resolver) { SCMResolver.new }
10 |
11 | before do
12 | Rake::Task.define_task("deploy:check")
13 | Rake::Task.define_task("deploy:new_release_path")
14 | Rake::Task.define_task("deploy:set_current_revision")
15 | Rake::Task.define_task("deploy:set_current_revision_time")
16 | set :scm, SCMResolver::DEFAULT_GIT
17 | end
18 |
19 | after do
20 | Rake::Task.clear
21 | Capistrano::Configuration.reset!
22 | end
23 |
24 | context "default scm, no plugin installed" do
25 | it "emits a warning" do
26 | expect { resolver.resolve }.to output(/will not load the git scm/i).to_stderr
27 | end
28 |
29 | it "activates the git scm", capture_io: true do
30 | resolver.resolve
31 | expect(Rake::Task["git:wrapper"]).not_to be_nil
32 | end
33 |
34 | it "sets :scm to :git", capture_io: true do
35 | resolver.resolve
36 | expect(fetch(:scm)).to eq(:git)
37 | end
38 | end
39 |
40 | context "default scm, git plugin installed" do
41 | before do
42 | install_plugin Capistrano::SCM::Git
43 | end
44 |
45 | it "emits no warning" do
46 | expect { resolver.resolve }.not_to output.to_stderr
47 | end
48 |
49 | it "deletes :scm" do
50 | resolver.resolve
51 | expect(fetch(:scm)).to be_nil
52 | end
53 | end
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/docs/documentation/advanced-features/overriding-capistrano-tasks/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: Overriding Capistrano tasks
3 | layout: default
4 | ---
5 |
6 | When re-defining a task in Capistrano v2, the original task was replaced. The
7 | Rake DSL on which Capistrano v3 is built is additive however, which means that
8 | given the following definitions
9 |
10 | ```ruby
11 | task :foo do
12 | puts "foo"
13 | end
14 |
15 | task :foo do
16 | puts "bar"
17 | end
18 | ```
19 |
20 | Will print both `foo` and `bar`.
21 |
22 | But it is also possible to completely clear a task and then re-defining it
23 | from scratch. A `Rake::Task` provides the `clear` method for this, which
24 | internally performs three separate actions:
25 |
26 | - `clear_prerequisites`
27 | - `clear_actions`
28 | - `clear_comments`
29 |
30 | Clearing the prerequisites (i.e. any dependencies that may have been defined
31 | for a task) is probably not what you want, though. Let's say, for example,
32 | that you want to re-define the `deploy:revert_release` task, which is defined
33 | as follows:
34 |
35 | ```ruby
36 | task :revert_release => :rollback_release_path do
37 | # ...
38 | end
39 | ```
40 |
41 | Calling `clear` on this task and then re-defining it results in
42 | `rollback_release_path` never being called, thus breaking rollback behavior.
43 |
44 | Under most circumstances, you will simply want to use `clear_actions`, which
45 | removes the specified task's behaviour, but does not alter it's dependencies
46 | or comments:
47 |
48 | ```ruby
49 | task :init do
50 | puts "init"
51 | end
52 |
53 | task :foo => :init do
54 | puts "foo"
55 | end
56 |
57 | Rake::Task["foo"].clear_actions
58 | task :foo do
59 | puts "bar"
60 | end
61 | ```
62 |
63 | Running the `foo` task will print
64 |
65 | ```ruby
66 | init
67 | bar
68 | ```
69 |
70 | ---
71 |
--------------------------------------------------------------------------------
/lib/capistrano/scm/svn.rb:
--------------------------------------------------------------------------------
1 | require "capistrano/scm/plugin"
2 |
3 | class Capistrano::SCM::Svn < Capistrano::SCM::Plugin
4 | def register_hooks
5 | after "deploy:new_release_path", "svn:create_release"
6 | before "deploy:check", "svn:check"
7 | before "deploy:set_current_revision", "svn:set_current_revision"
8 | end
9 |
10 | def define_tasks
11 | eval_rakefile File.expand_path("../tasks/svn.rake", __FILE__)
12 | end
13 |
14 | def svn(*args)
15 | args.unshift(:svn)
16 | args.push "--username #{fetch(:svn_username)}" if fetch(:svn_username)
17 | args.push "--password #{fetch(:svn_password)}" if fetch(:svn_password)
18 | args.push "--revision #{fetch(:svn_revision)}" if fetch(:svn_revision)
19 | backend.execute(*args)
20 | end
21 |
22 | def repo_mirror_exists?
23 | backend.test " [ -d #{repo_path}/.svn ] "
24 | end
25 |
26 | def check_repo_is_reachable
27 | svn_username = fetch(:svn_username) ? "--username #{fetch(:svn_username)}" : ""
28 | svn_password = fetch(:svn_password) ? "--password #{fetch(:svn_password)}" : ""
29 | backend.test :svn, :info, repo_url, svn_username, svn_password
30 | end
31 |
32 | def clone_repo
33 | svn :checkout, repo_url, repo_path.to_s
34 | end
35 |
36 | def update_mirror
37 | # Switch the repository URL if necessary.
38 | repo_mirror_url = fetch_repo_mirror_url
39 | svn :switch, repo_url unless repo_mirror_url == repo_url
40 | svn :update
41 | end
42 |
43 | def archive_to_release_path
44 | svn :export, "--force", ".", release_path
45 | end
46 |
47 | def fetch_revision
48 | backend.capture(:svnversion, repo_path.to_s)
49 | end
50 |
51 | def fetch_repo_mirror_url
52 | backend.capture(:svn, :info, repo_path.to_s).each_line do |line|
53 | return $1 if /\AURL: (.*)\n\z/ =~ line
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/lib/capistrano/configuration/plugin_installer.rb:
--------------------------------------------------------------------------------
1 | # Encapsulates the logic for installing plugins into Capistrano. Plugins must
2 | # simply conform to a basic API; the PluginInstaller takes care of invoking the
3 | # API at appropriate times.
4 | #
5 | # This class is not used directly; instead it is typically accessed via the
6 | # `install_plugin` method of the Capistrano DSL.
7 | #
8 | module Capistrano
9 | class Configuration
10 | class PluginInstaller
11 | # "Installs" a Plugin into Capistrano by loading its tasks, hooks, and
12 | # defaults at the appropriate time. The hooks in particular can be
13 | # skipped, if you want full control over when and how the plugin's tasks
14 | # are executed. Simply pass `load_hooks:false` to opt out.
15 | #
16 | # The plugin class or instance may be provided. These are equivalent:
17 | #
18 | # install(Capistrano::SCM::Git)
19 | # install(Capistrano::SCM::Git.new)
20 | #
21 | # Note that the :load_immediately flag is for internal use only and will
22 | # be removed in an upcoming release.
23 | #
24 | def install(plugin, load_hooks: true, load_immediately: false)
25 | plugin = plugin.is_a?(Class) ? plugin.new : plugin
26 |
27 | plugin.define_tasks
28 | plugin.register_hooks if load_hooks
29 | @scm_installed ||= provides_scm?(plugin)
30 |
31 | if load_immediately
32 | plugin.set_defaults
33 | else
34 | Rake::Task.define_task("load:defaults") do
35 | plugin.set_defaults
36 | end
37 | end
38 | end
39 |
40 | def scm_installed?
41 | @scm_installed
42 | end
43 |
44 | private
45 |
46 | def provides_scm?(plugin)
47 | plugin.respond_to?(:scm?) && plugin.scm?
48 | end
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/capistrano/dsl/task_enhancements.rb:
--------------------------------------------------------------------------------
1 | require "capistrano/upload_task"
2 |
3 | module Capistrano
4 | module TaskEnhancements
5 | def before(task, prerequisite, *args, &block)
6 | prerequisite = Rake::Task.define_task(prerequisite, *args, &block) if block_given?
7 | Rake::Task[task].enhance [prerequisite]
8 | end
9 |
10 | def after(task, post_task, *args, &block)
11 | Rake::Task.define_task(post_task, *args, &block) if block_given?
12 | task = Rake::Task[task]
13 | task.enhance do
14 | post = Rake.application.lookup(post_task, task.scope)
15 | raise ArgumentError, "Task #{post_task.inspect} not found" unless post
16 | post.invoke
17 | end
18 | end
19 |
20 | def define_remote_file_task(task, target_roles)
21 | Capistrano::UploadTask.define_task(task) do |t|
22 | prerequisite_file = t.prerequisites.first
23 | file = shared_path.join(t.name)
24 |
25 | on roles(target_roles) do
26 | unless test "[ -f #{file.to_s.shellescape} ]"
27 | info "Uploading #{prerequisite_file} to #{file}"
28 | upload! File.open(prerequisite_file), file
29 | end
30 | end
31 | end
32 | end
33 |
34 | def ensure_stage
35 | Rake::Task.define_task(:ensure_stage) do
36 | unless stage_set?
37 | puts t(:stage_not_set)
38 | exit 1
39 | end
40 | end
41 | end
42 |
43 | def tasks_without_stage_dependency
44 | stages + default_tasks
45 | end
46 |
47 | def default_tasks
48 | %w{install}
49 | end
50 |
51 | def exit_deploy_because_of_exception(ex)
52 | warn t(:deploy_failed, ex: ex.message)
53 | invoke "deploy:failed"
54 | exit(false)
55 | end
56 |
57 | def deploying?
58 | fetch(:deploying, false)
59 | end
60 | end
61 | end
62 |
--------------------------------------------------------------------------------
/docs/assets/css/github.css:
--------------------------------------------------------------------------------
1 | /**
2 | * GitHub theme
3 | *
4 | * @author Craig Campbell
5 | * @version 1.0.4
6 | */
7 | pre {
8 | border: 1px solid #ccc;
9 | word-wrap: break-word;
10 | padding: 6px 10px;
11 | line-height: 19px;
12 | margin-bottom: 20px;
13 | }
14 |
15 | code {
16 | border: 1px solid #eaeaea;
17 | margin: 0px 2px;
18 | padding: 0px 5px;
19 | font-size: 12px;
20 | }
21 |
22 | pre code {
23 | border: 0px;
24 | padding: 0px;
25 | margin: 0px;
26 | -moz-border-radius: 0px;
27 | -webkit-border-radius: 0px;
28 | border-radius: 0px;
29 | }
30 |
31 | pre, code {
32 | font-family: Consolas, 'Liberation Mono', Courier, monospace;
33 | color: #333;
34 | background: #f8f8f8;
35 | -moz-border-radius: 3px;
36 | -webkit-border-radius: 3px;
37 | border-radius: 3px;
38 | }
39 |
40 | pre, pre code {
41 | font-size: 13px;
42 | }
43 |
44 | pre .comment {
45 | color: #998;
46 | }
47 |
48 | pre .support {
49 | color: #0086B3;
50 | }
51 |
52 | pre .tag, pre .tag-name {
53 | color: navy;
54 | }
55 |
56 | pre .keyword, pre .css-property, pre .vendor-prefix, pre .sass, pre .class, pre .id, pre .css-value, pre .entity.function, pre .storage.function {
57 | font-weight: bold;
58 | }
59 |
60 | pre .css-property, pre .css-value, pre .vendor-prefix, pre .support.namespace {
61 | color: #333;
62 | }
63 |
64 | pre .constant.numeric, pre .keyword.unit, pre .hex-color {
65 | font-weight: normal;
66 | color: #099;
67 | }
68 |
69 | pre .entity.class {
70 | color: #458;
71 | }
72 |
73 | pre .entity.id, pre .entity.function {
74 | color: #900;
75 | }
76 |
77 | pre .attribute, pre .variable {
78 | color: teal;
79 | }
80 |
81 | pre .string, pre .support.value {
82 | font-weight: normal;
83 | color: #d14;
84 | }
85 |
86 | pre .regexp {
87 | color: #009926;
88 | }
89 |
--------------------------------------------------------------------------------
/docs/documentation/getting-started/user-input/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: User Input
3 | layout: default
4 | ---
5 |
6 | User input can be required in a task or during configuration:
7 |
8 | ```ruby
9 | # used in a configuration
10 | ask(:database_name, "default_database_name")
11 |
12 | # used in a task
13 | desc "Ask about breakfast"
14 | task :breakfast do
15 | ask(:breakfast, "pancakes")
16 | on roles(:all) do |h|
17 | execute "echo \"$(whoami) wants #{fetch(:breakfast)} for breakfast!\""
18 | end
19 | end
20 | ```
21 |
22 | When using `ask` to get user input, you can pass `echo: false` to prevent the
23 | input from being displayed. This option should be used to ask the user for
24 | passwords and other sensitive data during a deploy run.
25 |
26 | ```ruby
27 | ask(:database_password, 'default_password', echo: false)
28 | ```
29 |
30 |
31 | The symbol passed as a parameter will be printed as text for the user and the
32 | input will be saved to this variable:
33 |
34 | ```ruby
35 | ask(:database_encoding, 'UTF-8')
36 | # Please enter :database_encoding (UTF-8):
37 |
38 | fetch(:database_encoding)
39 | # => contains the user input (or the default)
40 | # once the above line got executed
41 | ```
42 |
43 |
44 | You can use `ask` to set a server- or role-specific configuration variable.
45 |
46 | ```ruby
47 | ask(:password, nil)
48 | server 'example.com', user: 'ssh_user_name', port: 22, password: fetch(:password), roles: %w{web app db}
49 | ```
50 |
51 |
52 | You can also show your own message by using `prompt` option:
53 |
54 | ```ruby
55 | ask(:breakfast, "pancakes", prompt: "What's for breakfast?")
56 | ```
57 |
58 | **Important!** `ask` will not prompt the user immediately. The question is
59 | deferred until the first time `fetch` is used to obtain the setting. That means
60 | you can `ask` for many variables, but only the variables used by your task(s)
61 | will actually prompt the user for input.
62 |
--------------------------------------------------------------------------------
/lib/capistrano/configuration/question.rb:
--------------------------------------------------------------------------------
1 | module Capistrano
2 | class Configuration
3 | class Question
4 | def initialize(key, default, options={})
5 | @key = key
6 | @default = default
7 | @options = options
8 | end
9 |
10 | def call
11 | ask_question
12 | value_or_default
13 | end
14 |
15 | private
16 |
17 | attr_reader :key, :default, :options
18 |
19 | def ask_question
20 | $stdout.print question
21 | $stdout.flush
22 | end
23 |
24 | def value_or_default
25 | if response.empty?
26 | default
27 | else
28 | response
29 | end
30 | end
31 |
32 | def response
33 | return @response if defined? @response
34 |
35 | @response = (gets || "").chomp
36 | end
37 |
38 | def gets
39 | return unless stdin.tty?
40 |
41 | if echo?
42 | stdin.gets
43 | else
44 | stdin.noecho(&:gets).tap { $stdout.print "\n" }
45 | end
46 | rescue Errno::EIO
47 | # when stdio gets closed
48 | return
49 | end
50 |
51 | def question
52 | if prompt && default.nil?
53 | I18n.t(:question_prompt, key: prompt, scope: :capistrano)
54 | elsif prompt
55 | I18n.t(:question_prompt_default, key: prompt, default_value: default, scope: :capistrano)
56 | elsif default.nil?
57 | I18n.t(:question, key: key, scope: :capistrano)
58 | else
59 | I18n.t(:question_default, key: key, default_value: default, scope: :capistrano)
60 | end
61 | end
62 |
63 | def echo?
64 | (options || {}).fetch(:echo, true)
65 | end
66 |
67 | def stdin
68 | (options || {}).fetch(:stdin, $stdin)
69 | end
70 |
71 | def prompt
72 | (options || {}).fetch(:prompt, nil)
73 | end
74 | end
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/docs/documentation/getting-started/version-locking/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: Version Locking
3 | layout: default
4 | ---
5 |
6 | Capistrano will, by default, include a `lock` command at the top of `deploy.rb`. This checks that the version of Capistrano running the configuration is the same as was intended to run it.
7 |
8 | The reasoning for this is that, in a pre-Bundler world, or when Bundler is not being used, Capistrano could behave in an unexpected and unclear manner with an incompatible configuration. Even today, it is easy to run Capistrano without `bundle exec` or a binstub (`bin/cap`, obtained through `bundle binstub capistrano`), resulting in unexpected behavior.
9 |
10 | The syntax for the lock is the same as that used by Bundler in a Gemfile (see the Implementation section below).
11 |
12 | The simplest form is: `lock '3.9.0'`. This locks the configuration to the exact version given.
13 |
14 | The most useful form uses the pessimistic operator: `~> 3.9.0`. This allows the version of the last segment to be increased, and all prior segments are locked. For example, if you used `lock '~> 3.9.2'`, version `3.9.3` would be allowed, but `3.9.1`, `3.10.0`, and `4.0.0` would not. Generally, you will want to lock to the `major.minor` revision. This means that the major version cannot increase, but the minor version can, which is consistent with semantic versioning (which Capistrano follows, [loosely](https://github.com/capistrano/capistrano/pull/1894/files)).
15 |
16 | You can also use `>`, `<`, `<=`, `>=`, and `=` before the version, as in `lock '>= 3.9.0'`. These are useful if you want to lock to a specific set of rules.
17 |
18 | For more complex usage, you can combine operators. For example, you can write `lock ['>= 3.9.0', '< 3.9.10']`, which would allow everything from 3.9.0 to 3.9.9, but not 3.9.10 or greater.
19 |
20 | ## Implementation
21 |
22 | The code reuses RubyGems core [version comparison logic](https://ruby-doc.org/stdlib-2.4.2/libdoc/rubygems/rdoc/Gem/Dependency.html#method-i-3D-7E). So anything you can do in RubyGems, you can do here.
23 |
--------------------------------------------------------------------------------
/docs/assets/css/capistrano.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: "proxima-nova",sans-serif;
3 | }
4 |
5 | #carbonads img {
6 | float: left;
7 | margin-right: 1em;
8 | }
9 |
10 | #carbonads .carbon-text {
11 | display: block;
12 | font-size: 0.9em;
13 | color: silver;
14 | }
15 |
16 | #carbonads .carbon-poweredby {
17 | font-size: 0.75em;
18 | display: block;
19 | color: silver
20 | }
21 |
22 | .header {
23 | margin-bottom: 30px;
24 | padding-top: 20px;
25 | background-color: #1C1B39;
26 | min-height: 170px;
27 | }
28 |
29 | h1, h2, h3, h4, h5, h6 {
30 | font-weight: 400;
31 | font-family: 'Enriqueta', serif;
32 | }
33 |
34 | .highlighter-rouge {
35 | margin-bottom: 1.25em;
36 | }
37 |
38 | .alert-box {
39 | font-weight: normal;
40 | color: black;
41 |
42 | background-color: #d7ecfa;
43 | border: none;
44 | padding-left: 20px;
45 | }
46 |
47 | .alert-box.alert {
48 | background-color: #f7e4e1;
49 | color: black;
50 | }
51 |
52 | .alert-box.success {
53 | background-color: #e1faea;
54 | color: black;
55 | }
56 |
57 | .alert-box.secondary {
58 | background-color: #eaeaea;
59 | color: black;
60 | }
61 |
62 | /*p code, li code {
63 | padding: 3px;
64 | background-color: #E6E6E6;
65 | font-family: "droid-sans-mono", Consolas, Monaco, 'Andale Mono', monospace;
66 | font-size: 0.9em;
67 | color: #222;
68 | border-radius: 3px;
69 | }*/
70 |
71 | footer {
72 | padding: 1em;
73 | background-color: #222;
74 | color: #fff;
75 | }
76 |
77 | footer a, footer a:hover {
78 | color: #fff;
79 | }
80 |
81 |
82 | footer ul.social.icons {
83 | list-style-type: none;
84 | }
85 |
86 | li {
87 | margin-left: 2em;
88 | }
89 |
90 | footer ul.social.icons li.foundicon {
91 | float: left;
92 | margin-left: 10px;
93 | font-size: 5em;
94 | }
95 |
96 | footer ul.social.icons li.thanks-dnsimple {
97 | clear: left;
98 | }
99 |
100 | .github-widget {
101 | margin-bottom: 1em;
102 | }
103 |
104 | .github-box .github-box-download {
105 | height: 44px !important;
106 | }
107 |
108 | /*pre code {
109 | color: #fff;
110 | }*/
111 |
--------------------------------------------------------------------------------
/spec/lib/capistrano/application_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | describe Capistrano::Application do
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 "displays documentation URL as help banner", capture_io: true do
9 | flags "--help", "-h"
10 | expect($stdout.string.each_line.first).to match(/capistranorb.com/)
11 | end
12 |
13 | %w(quiet silent verbose).each do |switch|
14 | it "doesn't include --#{switch} in help", capture_io: true do
15 | flags "--help", "-h"
16 | expect($stdout.string).not_to match(/--#{switch}/)
17 | end
18 | end
19 |
20 | it "overrides the rake method, but still prints the rake version", capture_io: true do
21 | flags "--version", "-V"
22 | out = $stdout.string
23 | expect(out).to match(/\bCapistrano Version\b/)
24 | expect(out).to match(/\b#{Capistrano::VERSION}\b/)
25 | expect(out).to match(/\bRake Version\b/)
26 | expect(out).to match(/\b#{Rake::VERSION}\b/)
27 | end
28 |
29 | it "overrides the rake method, and sets the sshkit_backend to SSHKit::Backend::Printer", capture_io: true do
30 | flags "--dry-run", "-n"
31 | sshkit_backend = Capistrano::Configuration.fetch(:sshkit_backend)
32 | expect(sshkit_backend).to eq(SSHKit::Backend::Printer)
33 | end
34 |
35 | it "enables printing all config variables on command line parameter", capture_io: true do
36 | begin
37 | flags "--print-config-variables", "-p"
38 | expect(Capistrano::Configuration.fetch(:print_config_variables)).to be true
39 | ensure
40 | Capistrano::Configuration.reset!
41 | end
42 | end
43 |
44 | def flags(*sets)
45 | sets.each do |set|
46 | ARGV.clear
47 | @exit = catch(:system_exit) { command_line(*set) }
48 | end
49 | yield(subject.options) if block_given?
50 | end
51 |
52 | def command_line(*options)
53 | options.each { |opt| ARGV << opt }
54 | subject.define_singleton_method(:exit) do |*_args|
55 | throw(:system_exit, :exit)
56 | end
57 | subject.run
58 | subject.options
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/docs/documentation/advanced-features/property-filtering/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: Property Filtering
3 | layout: default
4 | ---
5 |
6 | Options may be passed to the `roles()` method (and implicitly in methods like
7 | `release_roles()` and `primary()`) that affect the set of servers returned. These options
8 | take the form of a Hash passed as the last parameter. Each of the key/value pairs in the
9 | hash are evaluated in the sequence they are declared and if all are true for a specific
10 | server then the server will be returned. The keys must always be symbols which have the
11 | following meaning:
12 |
13 | * `:filter`, or `:select`: The value is either a property keyname or a lambda which is
14 | called with the server as parameter. The value must return true for the server to be
15 | included.
16 |
17 | * `:exclude`: As above but the value must return false for the server to be included.
18 |
19 | * Any other symbol is taken as a server property name whose value must equal the given value.
20 | A lambda will not be called if one is supplied!
21 |
22 | ### Examples
23 |
24 | ```ruby
25 | server 'example1.com', roles: %w{web}, active: true
26 | server 'example2.com', roles: %w{web}
27 | server 'example3.com', roles: %w{app web}, active: true
28 | server 'example4.com', roles: %w{app}, primary: true
29 | server 'example5.com', roles: %w{db}, no_release: true, active: true
30 |
31 | task :demo do
32 | puts "All active release roles: 1,3"
33 | release_roles(:all, filter: :active).each do |r|
34 | puts "#{r.hostname}"
35 | end
36 | puts "All active roles: 1,3,5"
37 | roles(:all, active: true).each do |r|
38 | puts "#{r.hostname}"
39 | end
40 | puts "All web and db roles with selected names: 2,3"
41 | roles(:web, :db, select: ->(s){ s.hostname =~ /[234]/}).each do |r|
42 | puts "#{r.hostname}"
43 | end
44 | puts "All with no active property: 2,4"
45 | roles(:all, active: nil).each do |r|
46 | puts "#{r.hostname}"
47 | end
48 | puts "All except active: 2,4"
49 | roles(:all, exclude: :active).each do |r|
50 | puts "#{r.hostname}"
51 | end
52 | puts "All primary: 4"
53 | roles(:all, select: :primary).each do |r|
54 | puts "#{r.hostname}"
55 | end
56 | end
57 | ```
58 |
--------------------------------------------------------------------------------
/docs/documentation/faq/how-can-i-access-stage-configuration-variables/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: How can I access stage configuration variables?
3 | layout: default
4 | ---
5 |
6 | Configuration variables are accessed with the fetch method, like so:
7 |
8 | ```ruby
9 | local = fetch(:configuration_variable, _default_value_)
10 | ```
11 |
12 | This works fine when accessing configuration variables defined within the same file. For example accessing a previously set configuration variable defined in deploy.rb or accessing a set configuration variable in a stage file.
13 |
14 | The deploy.rb configuration is executed first and then the stage file(s) from config/deploy/*.rb are executed next. This means that the configuration variables set in deploy.rb are available to the stage files, but configuration variables created in a stage file are not available in deploy.rb. To access them they must be lazily loaded in deploy.rb. This works because all configuration variables (from both deploy.rb and the current stage file) have been defined by the time the tasks run and access the variables.
15 |
16 | For example, let's create a configuration variable in the production and staging files and access the current one from deploy.rb.
17 |
18 | config/deploy/production.rb
19 |
20 | ```ruby
21 | set :app_domain, "www.my_application.com"
22 | ```
23 |
24 | config/deploy/staging.rb
25 |
26 | ```ruby
27 | set :app_domain, "stage.application_test.com"
28 | ```
29 |
30 | These variables are not available in deploy.rb using `fetch(:nginx_port)` or `fetch(:app_domain)` because they are not defined when deploy.rb is executed. They can, however, be lazily loaded using a lambda in deploy.rb like this:
31 |
32 | config/deploy.rb
33 |
34 | ```ruby
35 | set :nginx_server_name, ->{ fetch(:app_domain) }
36 | set :puma_bind, ->{ "unix:/tmp/#{fetch(:app_domain)}.sock" }
37 | ```
38 |
39 | Now the `:nginx_server_name` and `:puma_bind` variables will be lazily assigned the values set in which ever stage file was used to deploy.
40 |
41 | If you need to create nested hashes, you might find `do/end` syntax more readable:
42 |
43 | ```ruby
44 | set :database_yml, -> do
45 | {
46 | production: {
47 | host: 'localhost'
48 | }
49 | }
50 | end
51 | ```
52 |
--------------------------------------------------------------------------------
/docs/documentation/harrow/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: What is Harrow?
3 | layout: default
4 | ---
5 |
6 | ### Harrow is a web-based platform for continuous integration and deployment built by the Capistrano team.
7 |
8 | There are many continuous integration tools in the world already, Harrow is
9 | ours. It is designed to "feel" familiar to Capistrano users.
10 |
11 | 
12 |
13 | Although Harrow is designed to work well for Capistrano-style use-cases, it is
14 | by no means limited to only being used for Capistrano, or even for deployment.
15 |
16 | Some of the features which make Harrow ideal for automating tools such as
17 | Capistrano:
18 |
19 | * A discrete concept of scripts and environments, allowing reuse of scripts in
20 | different settings using different configurations
21 | * A pure JSON-HAL API allowing integrations and tools
22 | * Powerful triggers and notifications allowing the construction of pipelines
23 | starting with git changes
24 |
25 | Harrow, much like Capistrano can also be used to:
26 |
27 | * To automate common tasks in software teams
28 | * To drive infrastructure provisioning tools such as *chef-solo*, *Ansible* or similar
29 |
30 | Again, like Capistrano, Harrow is also *very* scriptable, and can be integrated
31 | with any other software to form part of a larger tool.
32 |
33 | #### How To Use Harrow
34 |
35 | 1. Sign up for a Harrow [account](https://www.app.harrow.io/#/a/signin)
36 | 2. Connect your Git repository using our setup wizard
37 | 3. Choose "Capistrano" as the project template
38 |
39 | #### What does it cost, and how does that affect Capistrano
40 |
41 | Harrow has very reasonable [pricing](https://harrow.io/pricing/). As a
42 | comparison with other continuous integration tools, some of our customers have
43 | cut their monthly outgoing by a factor of 5 or more.
44 |
45 | For individual users, it's free to use forever. To work with collaborators in
46 | your projects, paid plans start at just $29/mo.
47 |
48 | Capistrano is unaffected by Harrow. Capistrano will remain liberally licensed
49 | (currently MIT) and will include discrete hooks offering Harrow to users
50 | without being intrusive.
51 |
--------------------------------------------------------------------------------
/lib/capistrano/i18n.rb:
--------------------------------------------------------------------------------
1 | require "i18n"
2 |
3 | en = {
4 | starting: "Starting",
5 | capified: "Capified",
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}: ",
14 | question_default: "Please enter %{key} (%{default_value}): ",
15 | question_prompt: "%{key}: ",
16 | question_prompt_default: "%{key} (%{default_value}): ",
17 | keeping_releases: "Keeping %{keep_releases} of %{releases} deployed releases on %{host}",
18 | skip_cleanup: "Skipping cleanup of invalid releases on %{host}; unexpected foldername found (should be timestamp)",
19 | wont_delete_current_release: "Current release was marked for being removed but it's going to be skipped on %{host}",
20 | no_current_release: "There is no current release present on %{host}",
21 | no_old_releases: "No old releases (keeping newest %{keep_releases}) on %{host}",
22 | linked_file_does_not_exist: "linked file %{file} does not exist on %{host}",
23 | cannot_rollback: "There are no older releases to rollback to",
24 | cannot_found_rollback_release: "Cannot rollback because release %{release} does not exist",
25 | mirror_exists: "The repository mirror is at %{at}",
26 | revision_log_message: "Branch %{branch} (at %{sha}) deployed as release %{release} by %{user}",
27 | rollback_log_message: "%{user} rolled back to release %{release}",
28 | deploy_failed: "The deploy has failed with an error: %{ex}",
29 | console: {
30 | welcome: "capistrano console - enter command to execute on %{stage}",
31 | bye: "bye"
32 | },
33 | error: {
34 | invalid_stage_name: '"%{name}" is a reserved word and cannot be used as a stage. Rename "%{path}" to something else.',
35 | user: {
36 | does_not_exist: "User %{user} does not exists",
37 | cannot_switch: "Cannot switch to user %{user}"
38 | }
39 | }
40 | }
41 |
42 | I18n.backend.store_translations(:en, capistrano: en)
43 |
44 | if I18n.respond_to?(:enforce_available_locales=)
45 | I18n.enforce_available_locales = true
46 | end
47 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches: [master]
5 | pull_request:
6 | jobs:
7 | spec:
8 | runs-on: ubuntu-latest
9 | strategy:
10 | matrix:
11 | ruby:
12 | [
13 | "2.3",
14 | "2.4",
15 | "2.5",
16 | "2.6",
17 | "2.7",
18 | "3.0",
19 | "3.1",
20 | "3.2",
21 | "3.3",
22 | "head",
23 | ]
24 | steps:
25 | - uses: actions/checkout@v4
26 | - name: Set up Ruby
27 | uses: ruby/setup-ruby@v1
28 | with:
29 | ruby-version: ${{ matrix.ruby }}
30 | bundler-cache: true
31 | - name: rake spec
32 | run: bundle exec rake spec
33 | spec-legacy:
34 | runs-on: ubuntu-20.04
35 | strategy:
36 | matrix:
37 | ruby: ["2.0", "2.1", "2.2"]
38 | steps:
39 | - uses: actions/checkout@v4
40 | - name: Set up Ruby
41 | uses: ruby/setup-ruby@v1
42 | with:
43 | ruby-version: ${{ matrix.ruby }}
44 | bundler-cache: true
45 | - name: rake spec
46 | run: bundle exec rake spec
47 | spec-all:
48 | runs-on: ubuntu-latest
49 | needs: [spec, spec-legacy]
50 | if: always()
51 | steps:
52 | - name: All tests ok
53 | if: ${{ !(contains(needs.*.result, 'failure')) }}
54 | run: exit 0
55 | - name: Some tests failed
56 | if: ${{ contains(needs.*.result, 'failure') }}
57 | run: exit 1
58 | rubocop:
59 | runs-on: ubuntu-latest
60 | steps:
61 | - uses: actions/checkout@v4
62 | - name: Set up Ruby
63 | uses: ruby/setup-ruby@v1
64 | with:
65 | ruby-version: "ruby" # latest-stable
66 | bundler-cache: true
67 | - name: rake rubocop
68 | run: bundle exec rake rubocop
69 | features:
70 | needs: [spec, spec-legacy]
71 | runs-on: ubuntu-latest
72 | steps:
73 | - uses: actions/checkout@v4
74 | - name: Set up Ruby
75 | uses: ruby/setup-ruby@v1
76 | with:
77 | ruby-version: "ruby" # latest-stable
78 | bundler-cache: true
79 | - name: rake features
80 | run: bundle exec rake features
81 |
--------------------------------------------------------------------------------
/docs/documentation/getting-started/structure/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: Structure
3 | layout: default
4 | ---
5 |
6 | Capistrano uses a strictly defined directory hierarchy on each remote server to organise the source code and other deployment-related data. The root path of this structure can be defined with the configuration variable `:deploy_to`.
7 |
8 | Assuming your `config/deploy.rb` contains this:
9 |
10 | ```ruby
11 | set :deploy_to, '/var/www/my_app_name'
12 | ```
13 |
14 |
15 | Then inspecting the directories inside `/var/www/my_app_name` looks like this:
16 |
17 | ```bash
18 | ├── current -> /var/www/my_app_name/releases/20150120114500/
19 | ├── releases
20 | │ ├── 20150080072500
21 | │ ├── 20150090083000
22 | │ ├── 20150100093500
23 | │ ├── 20150110104000
24 | │ └── 20150120114500
25 | ├── repo
26 | │ └──
27 | ├── revisions.log
28 | └── shared
29 | └──
30 | ```
31 |
32 |
33 | * `current` is a symlink pointing to the latest release. This symlink is
34 | updated at the end of a successful deployment. If the deployment fails in any
35 | step the `current` symlink still points to the old release.
36 |
37 | * `releases` holds all deployments in a timestamped folder. These folders are
38 | the target of the `current` symlink.
39 |
40 | * `repo` holds the version control system configured. In case of a git
41 | repository the content will be a raw git repository (e.g. objects, refs,
42 | etc.).
43 |
44 | * `revisions.log` is used to log every deploy or rollback. Each entry is
45 | timestamped and the executing user (`:local_user`, defaulting to the local
46 | username) is listed. Depending on your VCS data like branch names or revision
47 | numbers are listed as well.
48 |
49 | * `shared` contains the `linked_files` and `linked_dirs` which are symlinked
50 | into each release. This data persists across deployments and releases. It
51 | should be used for things like database configuration files and static and
52 | persistent user storage handed over from one release to the next.
53 |
54 | The application is completely contained within the path of `:deploy_to`. If
55 | you plan on deploying multiple applications to the same server, simply choose
56 | a different `:deploy_to` path.
57 |
--------------------------------------------------------------------------------
/lib/capistrano/doctor/variables_doctor.rb:
--------------------------------------------------------------------------------
1 | require "capistrano/doctor/output_helpers"
2 |
3 | module Capistrano
4 | module Doctor
5 | # Prints a table of all Capistrano variables and their current values. If
6 | # there are unrecognized variables, print warnings for them.
7 | class VariablesDoctor
8 | # These are keys that are recognized by Capistrano, but do not have values
9 | # set by default.
10 | WHITELIST = %i(
11 | application
12 | current_directory
13 | linked_dirs
14 | linked_files
15 | releases_directory
16 | repo_url
17 | repo_tree
18 | shared_directory
19 | ).freeze
20 | private_constant :WHITELIST
21 |
22 | include Capistrano::Doctor::OutputHelpers
23 |
24 | def initialize(env=Capistrano::Configuration.env)
25 | @env = env
26 | end
27 |
28 | def call
29 | title("Variables")
30 | values = inspect_all_values
31 |
32 | table(variables.keys.sort_by(&:to_s)) do |key, row|
33 | row.yellow if suspicious_keys.include?(key)
34 | row << key.inspect
35 | row << values[key]
36 | end
37 |
38 | puts if suspicious_keys.any?
39 |
40 | suspicious_keys.sort_by(&:to_s).each do |key|
41 | warning("#{key.inspect} is not a recognized Capistrano setting "\
42 | "(#{location(key)})")
43 | end
44 | end
45 |
46 | private
47 |
48 | attr_reader :env
49 |
50 | def variables
51 | env.variables
52 | end
53 |
54 | def inspect_all_values
55 | variables.keys.each_with_object({}) do |key, inspected|
56 | inspected[key] = if env.is_question?(key)
57 | ""
58 | else
59 | variables.peek(key).inspect
60 | end
61 | end
62 | end
63 |
64 | def suspicious_keys
65 | (variables.untrusted_keys & variables.unused_keys) - WHITELIST
66 | end
67 |
68 | def location(key)
69 | loc = variables.source_locations(key).first
70 | loc && loc.sub(/^#{Regexp.quote(Dir.pwd)}/, "").sub(/:in.*/, "")
71 | end
72 | end
73 | end
74 | end
75 |
--------------------------------------------------------------------------------
/spec/lib/capistrano/doctor/gems_doctor_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "capistrano/doctor/gems_doctor"
3 | require "airbrussh/version"
4 | require "sshkit/version"
5 | require "net/ssh/version"
6 |
7 | module Capistrano
8 | module Doctor
9 | describe GemsDoctor do
10 | let(:doc) { GemsDoctor.new }
11 |
12 | it "prints using 4-space indentation" do
13 | expect { doc.call }.to output(/^ {4}/).to_stdout
14 | end
15 |
16 | it "prints the Capistrano version" do
17 | expect { doc.call }.to\
18 | output(/capistrano\s+#{Regexp.quote(Capistrano::VERSION)}/).to_stdout
19 | end
20 |
21 | it "prints the Rake version" do
22 | expect { doc.call }.to\
23 | output(/rake\s+#{Regexp.quote(Rake::VERSION)}/).to_stdout
24 | end
25 |
26 | it "prints the SSHKit version" do
27 | expect { doc.call }.to\
28 | output(/sshkit\s+#{Regexp.quote(SSHKit::VERSION)}/).to_stdout
29 | end
30 |
31 | it "prints the Airbrussh version" do
32 | expect { doc.call }.to\
33 | output(/airbrussh\s+#{Regexp.quote(Airbrussh::VERSION)}/).to_stdout
34 | end
35 |
36 | it "prints the net-ssh version" do
37 | expect { doc.call }.to\
38 | output(/net-ssh\s+#{Regexp.quote(Net::SSH::Version::STRING)}/).to_stdout
39 | end
40 |
41 | it "warns that new version is available" do
42 | Gem.stubs(:latest_version_for).returns(Gem::Version.new("99.0.0"))
43 | expect { doc.call }.to output(/\(update available\)/).to_stdout
44 | end
45 |
46 | describe "Rake" do
47 | before do
48 | load File.expand_path("../../../../../lib/capistrano/doctor.rb",
49 | __FILE__)
50 | end
51 |
52 | after do
53 | Rake::Task.clear
54 | end
55 |
56 | it "has an doctor:gems task that calls GemsDoctor", capture_io: true do
57 | GemsDoctor.any_instance.expects(:call)
58 | Rake::Task["doctor:gems"].invoke
59 | end
60 |
61 | it "has a doctor task that depends on doctor:gems" do
62 | expect(Rake::Task["doctor"].prerequisites).to include("doctor:gems")
63 | end
64 | end
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/docs/_layouts/default.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ page.title }}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {% include metrics.html %}
28 |
29 | {% include header.html %}
30 |
31 |
32 |
33 | {% include navigation.html %}
34 |
35 |
36 |
37 |
{{ page.title }}
38 | {{ content }}
39 |
40 |
41 |
42 |
43 | {% include footer.html %}
44 |
45 |
46 |
47 |
48 |
49 |
50 | {% include google_tag_manager.html %}
51 |
52 |
53 |
--------------------------------------------------------------------------------
/lib/capistrano/dsl/paths.rb:
--------------------------------------------------------------------------------
1 | require "pathname"
2 | module Capistrano
3 | module DSL
4 | module Paths
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(fetch(:current_directory, "current"))
15 | end
16 |
17 | def releases_path
18 | deploy_path.join(fetch(:releases_directory, "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 | fetch(:repo_url)
40 | end
41 |
42 | def repo_path
43 | Pathname.new(fetch(:repo_path, ->() { deploy_path.join("repo") }))
44 | end
45 |
46 | def shared_path
47 | deploy_path.join(fetch(:shared_directory, "shared"))
48 | end
49 |
50 | def revision_log
51 | deploy_path.join("revisions.log")
52 | end
53 |
54 | def now
55 | env.timestamp.strftime("%Y%m%d%H%M%S")
56 | end
57 |
58 | def asset_timestamp
59 | env.timestamp.strftime("%Y%m%d%H%M.%S")
60 | end
61 |
62 | def linked_dirs(parent)
63 | paths = fetch(:linked_dirs)
64 | join_paths(parent, paths)
65 | end
66 |
67 | def linked_files(parent)
68 | paths = fetch(:linked_files)
69 | join_paths(parent, paths)
70 | end
71 |
72 | def linked_file_dirs(parent)
73 | map_dirnames(linked_files(parent))
74 | end
75 |
76 | def linked_dir_parents(parent)
77 | map_dirnames(linked_dirs(parent))
78 | end
79 |
80 | def join_paths(parent, paths)
81 | paths.map { |path| parent.join(path) }
82 | end
83 |
84 | def map_dirnames(paths)
85 | paths.map(&:dirname).uniq
86 | end
87 | end
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/docs/assets/js/foundation/foundation.cookie.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * jQuery Cookie Plugin v1.3
3 | * https://github.com/carhartl/jquery-cookie
4 | *
5 | * Copyright 2011, Klaus Hartl
6 | * Dual licensed under the MIT or GPL Version 2 licenses.
7 | * http://www.opensource.org/licenses/mit-license.php
8 | * http://www.opensource.org/licenses/GPL-2.0
9 | *
10 | * Modified to work with Zepto.js by ZURB
11 | */
12 | (function ($, document, undefined) {
13 |
14 | var pluses = /\+/g;
15 |
16 | function raw(s) {
17 | return s;
18 | }
19 |
20 | function decoded(s) {
21 | return decodeURIComponent(s.replace(pluses, ' '));
22 | }
23 |
24 | var config = $.cookie = function (key, value, options) {
25 |
26 | // write
27 | if (value !== undefined) {
28 | options = $.extend({}, config.defaults, options);
29 |
30 | if (value === null) {
31 | options.expires = -1;
32 | }
33 |
34 | if (typeof options.expires === 'number') {
35 | var days = options.expires, t = options.expires = new Date();
36 | t.setDate(t.getDate() + days);
37 | }
38 |
39 | value = config.json ? JSON.stringify(value) : String(value);
40 |
41 | return (document.cookie = [
42 | encodeURIComponent(key), '=', config.raw ? value : encodeURIComponent(value),
43 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
44 | options.path ? '; path=' + options.path : '',
45 | options.domain ? '; domain=' + options.domain : '',
46 | options.secure ? '; secure' : ''
47 | ].join(''));
48 | }
49 |
50 | // read
51 | var decode = config.raw ? raw : decoded;
52 | var cookies = document.cookie.split('; ');
53 | for (var i = 0, l = cookies.length; i < l; i++) {
54 | var parts = cookies[i].split('=');
55 | if (decode(parts.shift()) === key) {
56 | var cookie = decode(parts.join('='));
57 | return config.json ? JSON.parse(cookie) : cookie;
58 | }
59 | }
60 |
61 | return null;
62 | };
63 |
64 | config.defaults = {};
65 |
66 | $.removeCookie = function (key, options) {
67 | if ($.cookie(key) !== null) {
68 | $.cookie(key, null, options);
69 | return true;
70 | }
71 | return false;
72 | };
73 |
74 | })(Foundation.zj, document);
75 |
--------------------------------------------------------------------------------
/lib/capistrano/templates/stage.rb.erb:
--------------------------------------------------------------------------------
1 | # server-based syntax
2 | # ======================
3 | # Defines a single server with a list of roles and multiple properties.
4 | # You can define all roles on a single server, or split them:
5 |
6 | # server "example.com", user: "deploy", roles: %w{app db web}, my_property: :my_value
7 | # server "example.com", user: "deploy", roles: %w{app web}, other_property: :other_value
8 | # server "db.example.com", user: "deploy", roles: %w{db}
9 |
10 |
11 |
12 | # role-based syntax
13 | # ==================
14 |
15 | # Defines a role with one or multiple servers. The primary server in each
16 | # group is considered to be the first unless any hosts have the primary
17 | # property set. Specify the username and a domain or IP for the server.
18 | # Don't use `:all`, it's a meta role.
19 |
20 | # role :app, %w{deploy@example.com}, my_property: :my_value
21 | # role :web, %w{user1@primary.com user2@additional.com}, other_property: :other_value
22 | # role :db, %w{deploy@example.com}
23 |
24 |
25 |
26 | # Configuration
27 | # =============
28 | # You can set any configuration variable like in config/deploy.rb
29 | # These variables are then only loaded and set in this stage.
30 | # For available Capistrano configuration variables see the documentation page.
31 | # http://capistranorb.com/documentation/getting-started/configuration/
32 | # Feel free to add new variables to customise your setup.
33 |
34 |
35 |
36 | # Custom SSH Options
37 | # ==================
38 | # You may pass any option but keep in mind that net/ssh understands a
39 | # limited set of options, consult the Net::SSH documentation.
40 | # http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start
41 | #
42 | # Global options
43 | # --------------
44 | # set :ssh_options, {
45 | # keys: %w(/home/user_name/.ssh/id_rsa),
46 | # forward_agent: false,
47 | # auth_methods: %w(password)
48 | # }
49 | #
50 | # The server-based syntax can be used to override options:
51 | # ------------------------------------
52 | # server "example.com",
53 | # user: "user_name",
54 | # roles: %w{web app},
55 | # ssh_options: {
56 | # user: "user_name", # overrides user setting above
57 | # keys: %w(/home/user_name/.ssh/id_rsa),
58 | # forward_agent: false,
59 | # auth_methods: %w(publickey password)
60 | # # password: "please use keys"
61 | # }
62 |
--------------------------------------------------------------------------------
/docs/assets/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
38 |
--------------------------------------------------------------------------------
/docs/documentation/advanced-features/role-filtering/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: Role filtering
3 | layout: default
4 | ---
5 |
6 | You may have situations where you only want to deploy to servers matching
7 | a single role. For example, you may have changed some aspect of how the web
8 | role works, but don't want to trigger a deployment to your database servers.
9 |
10 | You can use the *role filter* to restrict Capistrano tasks to only servers
11 | match a given role or roles.
12 |
13 | If the filter matches no servers, no actions will be taken.
14 |
15 | If you specify a filter, it will match any servers that have that role, and
16 | it will run _all_ tasks for each of the roles that server has. For example,
17 | if you filtered for servers with the `web` role, and a server had both the
18 | `web` and `db` role, both the `web` and `db` role tasks would be executed on it.
19 |
20 | ### Specifying a role filter
21 |
22 | There are three ways to specify the role filter.
23 |
24 | #### Environment variable
25 |
26 | Capistrano will read the role filter from the environment variable `ROLES`
27 | if it is set. You can set it inline:
28 |
29 | ```bash
30 | ROLES=app,web cap production deploy
31 | ```
32 |
33 | Specify multiple roles by separating them with a comma.
34 |
35 | #### In configuration
36 |
37 | You can set the role filter inside your deploy configuration. For example,
38 | you can set the following inside `config/deploy.rb`:
39 |
40 | ```ruby
41 | set :filter, :roles => %w{app web}
42 | ```
43 |
44 | Note that you specify the filter as an array rather than as a comma-separated
45 | list of roles when using this method.
46 |
47 | Note that the keyname `:role` is also supported.
48 |
49 | #### On the command line
50 |
51 | In a similar way to using the environment variable, you can set the role
52 | filter by specifying it as a command line argument to `cap`:
53 |
54 | ```bash
55 | cap --roles=app,web production deploy
56 | ```
57 |
58 | Like the environment variable method, specify multiple roles by separating them
59 | with a comma.
60 |
61 | ### Using Regular Expressions
62 |
63 | Since role names are Ruby symbols they can legitimately contain any characters. However to
64 | allow multiple of them to be specified on one line we use the comma as a separator.
65 |
66 | To use a regular expression for a role filter begin and end the string with '/'. Because
67 | of the above these regular expressions may not contain a comma.
68 |
--------------------------------------------------------------------------------
/lib/capistrano/configuration/servers.rb:
--------------------------------------------------------------------------------
1 | require "set"
2 | require "capistrano/configuration"
3 | require "capistrano/configuration/filter"
4 |
5 | module Capistrano
6 | class Configuration
7 | class Servers
8 | include Enumerable
9 |
10 | def add_host(host, properties={})
11 | new_host = Server[host]
12 | new_host.port = properties[:port] if properties.key?(:port)
13 | # This matching logic must stay in sync with `Server#matches?`.
14 | key = ServerKey.new(new_host.hostname, new_host.port)
15 | existing = servers_by_key[key]
16 | if existing
17 | existing.user = new_host.user if new_host.user
18 | existing.with(properties)
19 | else
20 | servers_by_key[key] = new_host.with(properties)
21 | end
22 | end
23 |
24 | # rubocop:disable Security/MarshalLoad
25 | def add_role(role, hosts, options={})
26 | options_deepcopy = Marshal.dump(options.merge(roles: role))
27 | Array(hosts).each { |host| add_host(host, Marshal.load(options_deepcopy)) }
28 | end
29 | # rubocop:enable Security/MarshalLoad
30 |
31 | def roles_for(names)
32 | options = extract_options(names)
33 | s = Filter.new(:role, names).filter(servers_by_key.values)
34 | s.select { |server| server.select?(options) }
35 | end
36 |
37 | def role_properties_for(rolenames)
38 | roles = rolenames.to_set
39 | rps = Set.new unless block_given?
40 | roles_for(rolenames).each do |host|
41 | host.roles.intersection(roles).each do |role|
42 | [host.properties.fetch(role)].flatten(1).each do |props|
43 | if block_given?
44 | yield host, role, props
45 | else
46 | rps << (props || {}).merge(role: role, hostname: host.hostname)
47 | end
48 | end
49 | end
50 | end
51 | block_given? ? nil : rps
52 | end
53 |
54 | def fetch_primary(role)
55 | hosts = roles_for([role])
56 | hosts.find(&:primary) || hosts.first
57 | end
58 |
59 | def each
60 | servers_by_key.values.each { |server| yield server }
61 | end
62 |
63 | private
64 |
65 | ServerKey = Struct.new(:hostname, :port)
66 |
67 | def servers_by_key
68 | @servers_by_key ||= {}
69 | end
70 |
71 | def extract_options(array)
72 | array.last.is_a?(::Hash) ? array.pop : {}
73 | end
74 | end
75 | end
76 | end
77 |
--------------------------------------------------------------------------------
/docs/documentation/advanced-features/host-filtering/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: Host filtering
3 | layout: default
4 | ---
5 |
6 | You may encounter situations where you only want to deploy to a subset of
7 | the servers defined in your configuration. For example, a single server or
8 | set of servers may be misbehaving, and you want to re-deploy to just these
9 | servers without deploying to every server.
10 |
11 | You can use the *host filter* to restrict Capistrano tasks to only servers
12 | that match a given set of hostnames.
13 |
14 | If the filter matches no servers, no actions will be taken.
15 |
16 | If you specify a filter, it will match servers that have the listed hostnames,
17 | and it will run *all* the roles for each server. In other words, it only affects
18 | the servers the task runs on, not what tasks are run on a server.
19 |
20 | ### Specifying a host filter
21 |
22 | There are three ways to specify the host filter.
23 |
24 | #### Environment variable
25 |
26 | Capistrano will read the host filter from the environment variable `HOSTS`
27 | if it is set. You can set it inline:
28 |
29 | ```bash
30 | HOSTS=server1,server2 cap production deploy
31 | ```
32 |
33 | Specify multiple hosts by separating them with a comma.
34 |
35 | #### In configuration
36 |
37 | You can set the host filter inside your deploy configuration. For example,
38 | you can set the following inside `config/deploy.rb`:
39 |
40 | ```ruby
41 | set :filter, :hosts => %w{server1 server2}
42 | ```
43 |
44 | Note that you specify the filter as an array rather than as a comma-separated
45 | list of servers when using this method.
46 |
47 | Note that the keyname `:host` is also supported.
48 |
49 | #### On the command line
50 |
51 | In a similar way to using the environment variable, you can set the role
52 | filter by specifying it as a command line argument to `cap`:
53 |
54 | ```bash
55 | cap --hosts=server1,server2 production deploy
56 | ```
57 |
58 | Like the environment variable method, specify multiple servers by separating
59 | them with a comma.
60 |
61 | ### Using Regular Expressions
62 |
63 | If the host name in a filter doesn't match the set of valid characters for a DNS name
64 | (Given by the regular expression `/^[-A-Za-z0-9.]+$/`) then it's assumed to be a regular
65 | expression in standard Ruby syntax.
66 |
67 | For example, if you had three servers named localrubyserver1, localrubyserver2, and amazonrubyserver1, but only wanted to deploy to localrubyserver*, you call Capistrano with a regex:
68 |
69 | ```bash
70 | cap --hosts=^localrubyserver production deploy
71 | ```
72 |
--------------------------------------------------------------------------------
/spec/lib/capistrano/configuration/host_filter_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | module Capistrano
4 | class Configuration
5 | describe HostFilter do
6 | subject(:host_filter) { HostFilter.new(values) }
7 |
8 | let(:available) do
9 | [Server.new("server1"),
10 | Server.new("server2"),
11 | Server.new("server3"),
12 | Server.new("server4"),
13 | Server.new("server10")]
14 | end
15 |
16 | shared_examples "it filters hosts correctly" do |expected|
17 | it "filters correctly" do
18 | set = host_filter.filter(available)
19 | expect(set.map(&:hostname)).to eq(expected)
20 | end
21 | end
22 |
23 | describe "#filter" do
24 | context "with a string" do
25 | let(:values) { "server1" }
26 | it_behaves_like "it filters hosts correctly", %w{server1}
27 |
28 | context "and a single server" do
29 | let(:available) { Server.new("server1") }
30 | it_behaves_like "it filters hosts correctly", %w{server1}
31 | end
32 | end
33 |
34 | context "with a comma separated string" do
35 | let(:values) { "server1,server10" }
36 | it_behaves_like "it filters hosts correctly", %w{server1 server10}
37 | end
38 |
39 | context "with an array of strings" do
40 | let(:values) { %w{server1 server3} }
41 | it_behaves_like "it filters hosts correctly", %w{server1 server3}
42 | end
43 |
44 | context "with mixed splittable and unsplittable strings" do
45 | let(:values) { %w{server1 server2,server3} }
46 | it_behaves_like "it filters hosts correctly", %w{server1 server2 server3}
47 | end
48 |
49 | context "with a regexp" do
50 | let(:values) { "server[13]$" }
51 | it_behaves_like "it filters hosts correctly", %w{server1 server3}
52 | end
53 |
54 | context "with a regexp with line boundaries" do
55 | let(:values) { "^server" }
56 | it_behaves_like "it filters hosts correctly", %w{server1 server2 server3 server4 server10}
57 | end
58 |
59 | context "with a regexp with a comma" do
60 | let(:values) { 'server\d{1,3}$' }
61 | it_behaves_like "it filters hosts correctly", %w{server1 server2 server3 server4 server10}
62 | end
63 |
64 | context "without number" do
65 | let(:values) { "server" }
66 | it_behaves_like "it filters hosts correctly", %w{}
67 | end
68 | end
69 | end
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/spec/lib/capistrano/plugin_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "capistrano/plugin"
3 |
4 | module Capistrano
5 | describe Plugin do
6 | include Rake::DSL
7 | include Capistrano::DSL
8 |
9 | class DummyPlugin < Capistrano::Plugin
10 | def define_tasks
11 | task :hello do
12 | end
13 | end
14 |
15 | def register_hooks
16 | before "deploy:published", "hello"
17 | end
18 | end
19 |
20 | class ExternalTasksPlugin < Capistrano::Plugin
21 | def define_tasks
22 | eval_rakefile(
23 | File.expand_path("../../../support/tasks/plugin.rake", __FILE__)
24 | )
25 | end
26 |
27 | # Called from plugin.rake to demonstrate that helper methods work
28 | def hello
29 | set :plugin_result, "hello"
30 | end
31 | end
32 |
33 | before do
34 | # Define an example task to allow testing hooks
35 | task "deploy:published"
36 | end
37 |
38 | after do
39 | # Clean up any tasks or variables we created during the tests
40 | Rake::Task.clear
41 | Capistrano::Configuration.reset!
42 | end
43 |
44 | it "defines tasks when constructed" do
45 | install_plugin(DummyPlugin)
46 | expect(Rake::Task["hello"]).not_to be_nil
47 | end
48 |
49 | it "registers hooks when constructed" do
50 | install_plugin(DummyPlugin)
51 | expect(Rake::Task["deploy:published"].prerequisites).to include("hello")
52 | end
53 |
54 | it "skips registering hooks if load_hooks: false" do
55 | install_plugin(DummyPlugin, load_hooks: false)
56 | expect(Rake::Task["deploy:published"].prerequisites).to be_empty
57 | end
58 |
59 | it "doesn't call set_defaults immediately" do
60 | dummy = DummyPlugin.new
61 | install_plugin(dummy)
62 | dummy.expects(:set_defaults).never
63 | end
64 |
65 | it "calls set_defaults during load:defaults", capture_io: true do
66 | dummy = DummyPlugin.new
67 | dummy.expects(:set_defaults).once
68 | install_plugin(dummy)
69 | Rake::Task["load:defaults"].invoke
70 | end
71 |
72 | it "is able to load tasks from a .rake file", capture_io: true do
73 | install_plugin(ExternalTasksPlugin)
74 | Rake::Task["plugin_test"].invoke
75 | expect(fetch(:plugin_result)).to eq("hello")
76 | end
77 |
78 | it "exposes the SSHKit backend to subclasses" do
79 | SSHKit::Backend.expects(:current).returns(:backend)
80 | plugin = DummyPlugin.new
81 | expect(plugin.send(:backend)).to eq(:backend)
82 | end
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/docs/documentation/third-party-plugins/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: 3rd Party Plugins
3 | layout: default
4 | ---
5 |
6 | Here are some Capistrano plugins you might find useful.
7 | This list is neither complete nor audited in any way.
8 |
9 | You can help us expanding this list by sending us a pull request on
10 | GitHub.
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/lib/capistrano/doctor/output_helpers.rb:
--------------------------------------------------------------------------------
1 | module Capistrano
2 | module Doctor
3 | # Helper methods for pretty-printing doctor output to stdout. All output
4 | # (other than `title`) is indented by four spaces to facilitate copying and
5 | # pasting this output into e.g. GitHub or Stack Overflow to achieve code
6 | # formatting.
7 | module OutputHelpers
8 | class Row
9 | attr_reader :color
10 | attr_reader :values
11 |
12 | def initialize
13 | @values = []
14 | end
15 |
16 | def <<(value)
17 | values << value
18 | end
19 |
20 | def yellow
21 | @color = :yellow
22 | end
23 | end
24 |
25 | # Prints a table for a given array of records. For each record, the block
26 | # is yielded two arguments: the record and a Row object. To print values
27 | # for that record, add values using `row << "some value"`. A row can
28 | # optionally be highlighted in yellow using `row.yellow`.
29 | def table(records, &block)
30 | return if records.empty?
31 | rows = collect_rows(records, &block)
32 | col_widths = calculate_column_widths(rows)
33 |
34 | rows.each do |row|
35 | line = row.values.each_with_index.map do |value, col|
36 | value.to_s.ljust(col_widths[col])
37 | end.join(" ").rstrip
38 | line = color.colorize(line, row.color) if row.color
39 | puts line
40 | end
41 | end
42 |
43 | # Prints a title in blue with surrounding newlines.
44 | def title(text)
45 | # Use $stdout directly to bypass the indentation that our `puts` does.
46 | $stdout.puts(color.colorize("\n#{text}\n", :blue))
47 | end
48 |
49 | # Prints text in yellow.
50 | def warning(text)
51 | puts color.colorize(text, :yellow)
52 | end
53 |
54 | # Override `Kernel#puts` to prepend four spaces to each line.
55 | def puts(string=nil)
56 | $stdout.puts(string.to_s.gsub(/^/, " "))
57 | end
58 |
59 | private
60 |
61 | def collect_rows(records)
62 | records.map do |rec|
63 | Row.new.tap { |row| yield(rec, row) }
64 | end
65 | end
66 |
67 | def calculate_column_widths(rows)
68 | num_columns = rows.map { |row| row.values.length }.max
69 | Array.new(num_columns) do |col|
70 | rows.map { |row| row.values[col].to_s.length }.max
71 | end
72 | end
73 |
74 | def color
75 | @color ||= SSHKit::Color.new($stdout)
76 | end
77 | end
78 | end
79 | end
80 |
--------------------------------------------------------------------------------
/docs/assets/css/dreamweaver.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Dreamweaver theme
3 | *
4 | * @author Sean Coker
5 | * @url http://seancoker.com
6 | * @version 1.0
7 | */
8 |
9 | pre {
10 | /* original is white background with no border */
11 | background-color: #fff;
12 | word-wrap: break-word;
13 | margin: 0;
14 | padding: 10px;
15 | color: #000;
16 | font-size: 13px;
17 | line-height: 16px;
18 | margin-bottom: 20px
19 | }
20 |
21 | pre, code {
22 | font-family: monospace;
23 | }
24 |
25 | pre .comment {
26 | color: #888;
27 | }
28 |
29 | pre .support {
30 | color: #cd57d5;
31 | }
32 |
33 | pre .constant.numeric, pre .php.embedded {
34 | color: #fa0002;
35 | font-weight: bold;
36 | }
37 |
38 | pre .keyword, pre .constant.language {
39 | color: #000789;
40 | font-weight: bold;
41 | }
42 |
43 | pre .selector, pre .support.property, pre .entity.name.function {
44 | color: #000;
45 | }
46 |
47 | pre .storage.function, pre .variable.self, pre .support.function, pre .constant.language {
48 | color: #000;
49 | font-weight: bold;
50 | }
51 |
52 | pre .string {
53 | color: #0d43fa;
54 | font-weight: normal;
55 | }
56 |
57 | pre .css-property + span, pre .keyword.unit, pre .support.css-value {
58 | color: #0d43fa !important;
59 | font-weight: normal !important;
60 | }
61 |
62 | pre .entity.tag.style + .string, pre .php.embedded .constant.language, pre .php.embedded .keyword {
63 | color: #37a348 !important;
64 | }
65 |
66 | pre .support.method {
67 | color: #2bd5bb;
68 | }
69 |
70 | pre .entity.name {
71 | color: #fd74e0;
72 | }
73 |
74 | pre .support.css-property, pre .support.tag-name, pre .support.tag, pre .support.attribute, pre .support.attribute + .operator {
75 | color: #000789;
76 | }
77 |
78 | pre .storage.module, pre .storage.class {
79 | color: #122573;
80 | font-weight: bold;
81 | }
82 |
83 | pre .css.embedded .support.tag, pre .css.embedded .style.tag {
84 | color: #cd57d5;
85 | }
86 |
87 | pre .keyword.operator {
88 | color: #2852eb;
89 | font-weight: normal;
90 | }
91 |
92 | pre .php.embedded .variable, pre .php.embedded .storage.function {
93 | color: #0d43fa;
94 | font-weight: normal;
95 | }
96 |
97 | pre .php.embedded .string, pre .js.embedded .tag.script {
98 | color: #c4001e;
99 | }
100 |
101 | pre .php.embedded .comment {
102 | color: #f4b441;
103 | font-weight: normal;
104 | }
105 |
106 | pre .php.embedded .function.name {
107 | color: #000;
108 | font-weight: normal;
109 | }
110 |
--------------------------------------------------------------------------------
/UPGRADING-3.7.md:
--------------------------------------------------------------------------------
1 | # Capistrano 3.7.0 upgrade guide
2 |
3 | ## The :scm variable is deprecated
4 |
5 | Up until now, Capistrano's SCM was configured using the `:scm` variable:
6 |
7 | ```ruby
8 | # This is now deprecated
9 | set :scm, :svn
10 | ```
11 |
12 | To avoid deprecation warnings:
13 |
14 | 1. Remove `set :scm, ...` from your Capistrano configuration.
15 | 2. Add *one* of the following SCM declarations to your `Capfile` after `require "capistrano/deploy"`:
16 |
17 | ```ruby
18 | # To use Git
19 | require "capistrano/scm/git"
20 | install_plugin Capistrano::SCM::Git
21 |
22 | # To use Mercurial
23 | require "capistrano/scm/hg"
24 | install_plugin Capistrano::SCM::Hg
25 |
26 | # To use Subversion
27 | require "capistrano/scm/svn"
28 | install_plugin Capistrano::SCM::Svn
29 | ```
30 |
31 | ## This is the last release where Git is the automatic default
32 |
33 | If you do not specify an SCM, Capistrano assumes Git. However this behavior is
34 | now deprecated. Add this to your Capfile to avoid deprecation warnings:
35 |
36 | ```ruby
37 | require "capistrano/scm/git"
38 | install_plugin Capistrano::SCM::Git
39 | ```
40 |
41 | ## :git_strategy, :hg_strategy, and :svn_strategy are removed
42 |
43 | Capistrano 3.7.0 has a rewritten SCM system that relies on "plugins". This
44 | system is more flexible than the old "strategy" system that only allowed certain
45 | parts of SCM tasks to be customized.
46 |
47 | If your deployment relies on a custom SCM strategy, you will need to rewrite
48 | that strategy to be a full-fledged SCM plugin instead. There is a fairly
49 | straightforward migration path: write your plugin to be a subclass of the
50 | built-in SCM that you want to customize. For example:
51 |
52 | ```ruby
53 | require "capistrano/scm/git"
54 |
55 | class MyCustomGit < Capistrano::SCM::Git
56 | # Override the methods you wish to customize, e.g.:
57 | def clone_repo
58 | # ...
59 | end
60 | end
61 | ```
62 |
63 | Then use your plugin in by loading it in the Capfile:
64 |
65 | ```ruby
66 | require_relative "path/to/my_custom_git.rb"
67 | install_plugin MyCustomGit
68 | ```
69 |
70 | ## Existing third-party SCMs are deprecated
71 |
72 | If you are using a third-party SCM, you can continue using it without
73 | changes, but you will see deprecation warnings. Contact the maintainer of the
74 | third-party SCM gem and ask them about modifying the gem to work with the new
75 | Capistrano 3.7.0 SCM plugin system.
76 |
77 | ## remote_file is removed
78 |
79 | The `remote_file` method is no longer in Capistrano 3.7.0. You can read the
80 | discussion that led to its removal here:
81 | [issue 762](https://github.com/capistrano/capistrano/issues/762).
82 |
83 | There is no direct replacement. To migrate to 3.7.0, you will need to rewrite
84 | any parts of your deployment that use `remote_file` to use a different
85 | mechanism for uploading files. Consider using the `upload!` method directly in
86 | a procedural fashion instead.
87 |
--------------------------------------------------------------------------------
/spec/lib/capistrano/configuration/role_filter_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | module Capistrano
4 | class Configuration
5 | describe RoleFilter do
6 | subject(:role_filter) { RoleFilter.new(values) }
7 |
8 | let(:available) do
9 | [
10 | Server.new("server1").add_roles(%i(web db)),
11 | Server.new("server2").add_role(:web),
12 | Server.new("server3").add_role(:redis),
13 | Server.new("server4").add_role(:db),
14 | Server.new("server5").add_role(:stageweb),
15 | Server.new("server6").add_role(:"db.new")
16 | ]
17 | end
18 |
19 | shared_examples "it filters roles correctly" do |expected_size, expected|
20 | it "filters correctly" do
21 | set = role_filter.filter(available)
22 | expect(set.size).to eq(expected_size)
23 | expect(set.map(&:hostname)).to eq(expected)
24 | end
25 | end
26 |
27 | describe "#filter" do
28 | context "with a single role string" do
29 | let(:values) { "web" }
30 | it_behaves_like "it filters roles correctly", 2, %w{server1 server2}
31 | end
32 |
33 | context "with a single role" do
34 | let(:values) { [:web] }
35 | it_behaves_like "it filters roles correctly", 2, %w{server1 server2}
36 | end
37 |
38 | context "with multiple roles in a string" do
39 | let(:values) { "web,db" }
40 | it_behaves_like "it filters roles correctly", 3, %w{server1 server2 server4}
41 | end
42 |
43 | context "with multiple roles" do
44 | let(:values) { %i(web db) }
45 | it_behaves_like "it filters roles correctly", 3, %w{server1 server2 server4}
46 | end
47 |
48 | context "with a regex" do
49 | let(:values) { /red/ }
50 | it_behaves_like "it filters roles correctly", 1, %w{server3}
51 | end
52 |
53 | context "with a regex string" do
54 | let(:values) { "/red|web/" }
55 | it_behaves_like "it filters roles correctly", 4, %w{server1 server2 server3 server5}
56 | end
57 |
58 | context "with both a string and regex" do
59 | let(:values) { "db,/red/" }
60 | it_behaves_like "it filters roles correctly", 3, %w{server1 server3 server4}
61 | end
62 |
63 | context "with a dot wildcard" do
64 | let(:values) { "db.*" }
65 | it_behaves_like "it filters roles correctly", 0, %w{}
66 | end
67 |
68 | context "with a dot" do
69 | let(:values) { "db.new" }
70 | it_behaves_like "it filters roles correctly", 1, %w{server6}
71 | end
72 |
73 | context "with a dot wildcard regex" do
74 | let(:values) { "/db.*/" }
75 | it_behaves_like "it filters roles correctly", 3, %w{server1 server4 server6}
76 | end
77 | end
78 | end
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/spec/lib/capistrano/configuration/plugin_installer_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "capistrano/plugin"
3 | require "capistrano/scm/plugin"
4 |
5 | module Capistrano
6 | class Configuration
7 | class ExamplePlugin < Capistrano::Plugin
8 | def set_defaults
9 | set_if_empty :example_variable, "foo"
10 | end
11 |
12 | def define_tasks
13 | task :example
14 | task :example_prerequisite
15 | end
16 |
17 | def register_hooks
18 | before :example, :example_prerequisite
19 | end
20 | end
21 |
22 | class ExampleSCMPlugin < Capistrano::SCM::Plugin
23 | end
24 |
25 | describe PluginInstaller do
26 | include Capistrano::DSL
27 |
28 | let(:installer) { PluginInstaller.new }
29 | let(:options) { {} }
30 | let(:plugin) { ExamplePlugin.new }
31 |
32 | before do
33 | installer.install(plugin, **options)
34 | end
35 |
36 | after do
37 | Rake::Task.clear
38 | Capistrano::Configuration.reset!
39 | end
40 |
41 | context "installing plugin" do
42 | it "defines tasks" do
43 | expect(Rake::Task[:example]).to_not be_nil
44 | expect(Rake::Task[:example_prerequisite]).to_not be_nil
45 | end
46 |
47 | it "registers hooks" do
48 | task = Rake::Task[:example]
49 | expect(task.prerequisites).to eq([:example_prerequisite])
50 | end
51 |
52 | it "sets defaults when load:defaults is invoked", capture_io: true do
53 | expect(fetch(:example_variable)).to be_nil
54 | invoke "load:defaults"
55 | expect(fetch(:example_variable)).to eq("foo")
56 | end
57 |
58 | it "doesn't say an SCM is installed" do
59 | expect(installer.scm_installed?).to be_falsey
60 | end
61 | end
62 |
63 | context "installing plugin class" do
64 | let(:plugin) { ExamplePlugin }
65 |
66 | it "defines tasks" do
67 | expect(Rake::Task[:example]).to_not be_nil
68 | expect(Rake::Task[:example_prerequisite]).to_not be_nil
69 | end
70 | end
71 |
72 | context "installing plugin without hooks" do
73 | let(:options) { { load_hooks: false } }
74 |
75 | it "doesn't register hooks" do
76 | task = Rake::Task[:example]
77 | expect(task.prerequisites).to be_empty
78 | end
79 | end
80 |
81 | context "installing plugin and loading immediately" do
82 | let(:options) { { load_immediately: true } }
83 |
84 | it "sets defaults immediately" do
85 | expect(fetch(:example_variable)).to eq("foo")
86 | end
87 | end
88 |
89 | context "installing an SCM plugin" do
90 | let(:plugin) { ExampleSCMPlugin }
91 |
92 | it "says an SCM is installed" do
93 | expect(installer.scm_installed?).to be_truthy
94 | end
95 | end
96 | end
97 | end
98 | end
99 |
--------------------------------------------------------------------------------
/spec/lib/capistrano/scm_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | require "capistrano/scm"
4 |
5 | module RaiseNotImplementedMacro
6 | def raise_not_implemented_on(method)
7 | it "should raise NotImplemented on #{method}" do
8 | expect do
9 | subject.send(method)
10 | end.to raise_error(NotImplementedError)
11 | end
12 | end
13 | end
14 |
15 | RSpec.configure do
16 | include RaiseNotImplementedMacro
17 | end
18 |
19 | module DummyStrategy
20 | def test
21 | test!("you dummy!")
22 | end
23 | end
24 |
25 | module BlindStrategy; end
26 |
27 | module Capistrano
28 | describe SCM do
29 | let(:context) { mock }
30 |
31 | describe "#initialize" do
32 | subject { Capistrano::SCM.new(context, DummyStrategy) }
33 |
34 | it "should load the provided strategy" do
35 | context.expects(:test).with("you dummy!")
36 | subject.test
37 | end
38 | end
39 |
40 | describe "Convenience methods" do
41 | subject { Capistrano::SCM.new(context, BlindStrategy) }
42 |
43 | describe "#test!" do
44 | it "should return call test on the context" do
45 | context.expects(:test).with(:x)
46 | subject.test!(:x)
47 | end
48 | end
49 |
50 | describe "#repo_url" do
51 | it "should return the repo url according to the context" do
52 | context.expects(:repo_url).returns(:url)
53 | expect(subject.repo_url).to eq(:url)
54 | end
55 | end
56 |
57 | describe "#repo_path" do
58 | it "should return the repo path according to the context" do
59 | context.expects(:repo_path).returns(:path)
60 | expect(subject.repo_path).to eq(:path)
61 | end
62 | end
63 |
64 | describe "#release_path" do
65 | it "should return the release path according to the context" do
66 | context.expects(:release_path).returns("/path/to/nowhere")
67 | expect(subject.release_path).to eq("/path/to/nowhere")
68 | end
69 | end
70 |
71 | describe "#fetch" do
72 | it "should call fetch on the context" do
73 | context.expects(:fetch)
74 | subject.fetch(:branch)
75 | end
76 | end
77 | end
78 |
79 | describe "With a 'blind' strategy" do
80 | subject { Capistrano::SCM.new(context, BlindStrategy) }
81 |
82 | describe "#test" do
83 | raise_not_implemented_on(:test)
84 | end
85 |
86 | describe "#check" do
87 | raise_not_implemented_on(:check)
88 | end
89 |
90 | describe "#clone" do
91 | raise_not_implemented_on(:clone)
92 | end
93 |
94 | describe "#update" do
95 | raise_not_implemented_on(:update)
96 | end
97 |
98 | describe "#release" do
99 | raise_not_implemented_on(:release)
100 | end
101 | end
102 | end
103 | end
104 |
--------------------------------------------------------------------------------
/spec/lib/capistrano/doctor/servers_doctor_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "capistrano/doctor/servers_doctor"
3 |
4 | module Capistrano
5 | module Doctor
6 | describe ServersDoctor do
7 | include Capistrano::DSL
8 | let(:doc) { ServersDoctor.new }
9 |
10 | before { Capistrano::Configuration.reset! }
11 | after { Capistrano::Configuration.reset! }
12 |
13 | it "prints using 4-space indentation" do
14 | expect { doc.call }.to output(/^ {4}/).to_stdout
15 | end
16 |
17 | it "prints the number of defined servers" do
18 | role :app, %w(example.com)
19 | server "www@example.com:22"
20 |
21 | expect { doc.call }.to output(/Servers \(2\)/).to_stdout
22 | end
23 |
24 | describe "prints the server's details" do
25 | it "including username" do
26 | server "www@example.com"
27 | expect { doc.call }.to output(/www@example.com/).to_stdout
28 | end
29 |
30 | it "including port" do
31 | server "www@example.com:22"
32 | expect { doc.call }.to output(/www@example.com:22/).to_stdout
33 | end
34 |
35 | it "including roles" do
36 | role :app, %w(example.com)
37 | expect { doc.call }.to output(/example.com\s+\[:app\]/).to_stdout
38 | end
39 |
40 | it "including empty roles" do
41 | server "example.com"
42 | expect { doc.call }.to output(/example.com\s+\[\]/).to_stdout
43 | end
44 |
45 | it "including properties" do
46 | server "example.com", roles: %w(app db), primary: true
47 | expect { doc.call }.to \
48 | output(/example.com\s+\[:app, :db\]\s+\{ :primary => true \}/).to_stdout
49 | end
50 |
51 | it "including misleading role name alert" do
52 | server "example.com", roles: ["web app db"]
53 | warning_msg = 'Whitespace detected in role(s) :"web app db". ' \
54 | 'This might be a result of a mistyped "%w()" array literal'
55 |
56 | expect { doc.call }.to output(/#{Regexp.escape(warning_msg)}/).to_stdout
57 | end
58 | end
59 |
60 | it "doesn't fail for no servers" do
61 | expect { doc.call }.to output("\nServers (0)\n \n").to_stdout
62 | end
63 |
64 | describe "Rake" do
65 | before do
66 | load File.expand_path("../../../../../lib/capistrano/doctor.rb",
67 | __FILE__)
68 | end
69 |
70 | after do
71 | Rake::Task.clear
72 | end
73 |
74 | it "has an doctor:servers task that calls ServersDoctor", capture_io: true do
75 | ServersDoctor.any_instance.expects(:call)
76 | Rake::Task["doctor:servers"].invoke
77 | end
78 |
79 | it "has a doctor task that depends on doctor:servers" do
80 | expect(Rake::Task["doctor"].prerequisites).to \
81 | include("doctor:servers")
82 | end
83 | end
84 | end
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/lib/capistrano/doctor/servers_doctor.rb:
--------------------------------------------------------------------------------
1 | require "capistrano/doctor/output_helpers"
2 |
3 | module Capistrano
4 | module Doctor
5 | class ServersDoctor
6 | include Capistrano::Doctor::OutputHelpers
7 |
8 | def initialize(env=Capistrano::Configuration.env)
9 | @servers = env.servers.to_a
10 | end
11 |
12 | def call
13 | title("Servers (#{servers.size})")
14 | rwc = RoleWhitespaceChecker.new(servers)
15 |
16 | table(servers) do |server, row|
17 | sd = ServerDecorator.new(server)
18 |
19 | row << sd.uri_form
20 | row << sd.roles
21 | row << sd.properties
22 | row.yellow if rwc.any_has_whitespace?(server.roles)
23 | end
24 |
25 | if rwc.whitespace_roles.any?
26 | warning "\nWhitespace detected in role(s) #{rwc.whitespace_roles_decorated}. " \
27 | "This might be a result of a mistyped \"%w()\" array literal."
28 | end
29 | puts
30 | end
31 |
32 | private
33 |
34 | attr_reader :servers
35 |
36 | class RoleWhitespaceChecker
37 | attr_reader :whitespace_roles, :servers
38 |
39 | def initialize(servers)
40 | @servers = servers
41 | @whitespace_roles = find_whitespace_roles
42 | end
43 |
44 | def any_has_whitespace?(roles)
45 | roles.any? { |role| include_whitespace?(role) }
46 | end
47 |
48 | def include_whitespace?(role)
49 | role =~ /\s/
50 | end
51 |
52 | def whitespace_roles_decorated
53 | whitespace_roles.map(&:inspect).join(", ")
54 | end
55 |
56 | private
57 |
58 | def find_whitespace_roles
59 | servers.map(&:roles).flat_map(&:to_a).uniq
60 | .select { |role| include_whitespace?(role) }
61 | end
62 | end
63 |
64 | class ServerDecorator
65 | def initialize(server)
66 | @server = server
67 | end
68 |
69 | def uri_form
70 | [
71 | server.user,
72 | server.user && "@",
73 | server.hostname,
74 | server.port && ":",
75 | server.port
76 | ].compact.join
77 | end
78 |
79 | def roles
80 | server.roles.to_a.inspect
81 | end
82 |
83 | def properties
84 | return "" unless server.properties.keys.any?
85 | pretty_inspect(server.properties.to_h)
86 | end
87 |
88 | private
89 |
90 | attr_reader :server
91 |
92 | # Hashes with proper padding
93 | def pretty_inspect(element)
94 | return element.inspect unless element.is_a?(Hash)
95 |
96 | pairs_string = element.keys.map do |key|
97 | [pretty_inspect(key), pretty_inspect(element.fetch(key))].join(" => ")
98 | end.join(", ")
99 |
100 | "{ #{pairs_string} }"
101 | end
102 | end
103 | end
104 | end
105 | end
106 |
--------------------------------------------------------------------------------
/features/step_definitions/setup.rb:
--------------------------------------------------------------------------------
1 | Given(/^a test app with the default configuration$/) do
2 | TestApp.install
3 | end
4 |
5 | Given(/^a test app without any configuration$/) do
6 | TestApp.create_test_app
7 | end
8 |
9 | Given(/^servers with the roles app and web$/) do
10 | start_ssh_server
11 | wait_for_ssh_server
12 | end
13 |
14 | Given(/^a linked file "(.*?)"$/) do |file|
15 | # ignoring other linked files
16 | TestApp.append_to_deploy_file("set :linked_files, ['#{file}']")
17 | end
18 |
19 | Given(/^file "(.*?)" exists in shared path$/) do |file|
20 | file_shared_path = TestApp.shared_path.join(file)
21 | run_remote_ssh_command("mkdir -p #{file_shared_path.dirname}")
22 | run_remote_ssh_command("touch #{file_shared_path}")
23 | end
24 |
25 | Given(/^all linked files exists in shared path$/) do
26 | TestApp.linked_files.each do |linked_file|
27 | step %Q{file "#{linked_file}" exists in shared path}
28 | end
29 | end
30 |
31 | Given(/^file "(.*?)" does not exist in shared path$/) do |file|
32 | file_shared_path = TestApp.shared_path.join(file)
33 | run_remote_ssh_command("mkdir -p #{TestApp.shared_path}")
34 | run_remote_ssh_command("touch #{file_shared_path} && rm #{file_shared_path}")
35 | end
36 |
37 | Given(/^a custom task to generate a file$/) do
38 | TestApp.copy_task_to_test_app("spec/support/tasks/database.rake")
39 | end
40 |
41 | Given(/^a task which executes as root$/) do
42 | TestApp.copy_task_to_test_app("spec/support/tasks/root.rake")
43 | end
44 |
45 | Given(/config stage file has line "(.*?)"/) do |line|
46 | TestApp.append_to_deploy_file(line)
47 | end
48 |
49 | Given(/^the configuration is in a custom location$/) do
50 | TestApp.move_configuration_to_custom_location("app")
51 | end
52 |
53 | Given(/^a custom task that will simulate a failure$/) do
54 | safely_remove_file(TestApp.shared_path.join("failed"))
55 | TestApp.copy_task_to_test_app("spec/support/tasks/fail.rake")
56 | end
57 |
58 | Given(/^a custom task to run in the event of a failure$/) do
59 | safely_remove_file(TestApp.shared_path.join("failed"))
60 | TestApp.copy_task_to_test_app("spec/support/tasks/failed.rake")
61 | end
62 |
63 | Given(/^a stage file named (.+)$/) do |filename|
64 | TestApp.write_local_stage_file(filename)
65 | end
66 |
67 | Given(/^I make (\d+) deployments?$/) do |count|
68 | step "all linked files exists in shared path"
69 |
70 | @release_paths = (1..count.to_i).map do
71 | TestApp.cap("deploy")
72 | stdout, _stderr = run_remote_ssh_command("readlink #{TestApp.current_path}")
73 |
74 | stdout.strip
75 | end
76 | end
77 |
78 | Given(/^(\d+) valid existing releases$/) do |num|
79 | a_day = 86_400 # in seconds
80 | (1...num).each_slice(100) do |num_batch|
81 | dirs = num_batch.map do |i|
82 | offset = -(a_day * i)
83 | TestApp.release_path(TestApp.timestamp(offset))
84 | end
85 | run_remote_ssh_command("mkdir -p #{dirs.join(' ')}")
86 | end
87 | end
88 |
89 | Given(/^an invalid release named "(.+)"$/) do |filename|
90 | run_remote_ssh_command("mkdir -p #{TestApp.release_path(filename)}")
91 | end
92 |
--------------------------------------------------------------------------------
/docs/assets/css/social_foundicons.css:
--------------------------------------------------------------------------------
1 | /* font-face */
2 | @font-face {
3 | font-family: "SocialFoundicons";
4 | src: url("../fonts/social_foundicons.eot");
5 | src: url("../fonts/social_foundicons.eot?#iefix") format("embedded-opentype"), url("../fonts/social_foundicons.woff") format("woff"), url("../fonts/social_foundicons.ttf") format("truetype"), url("../fonts/social_foundicons.svg#SocialFoundicons") format("svg");
6 | font-weight: normal;
7 | font-style: normal;
8 | }
9 |
10 | /* global foundicon styles */
11 | [class*="foundicon-"] {
12 | display: inline;
13 | width: auto;
14 | height: auto;
15 | line-height: inherit;
16 | vertical-align: baseline;
17 | background-image: none;
18 | background-position: 0 0;
19 | background-repeat: repeat;
20 | }
21 |
22 | [class*="foundicon-"]:before {
23 | font-family: "SocialFoundicons";
24 | font-weight: normal;
25 | font-style: normal;
26 | text-decoration: inherit;
27 | }
28 |
29 | /* icons */
30 | .foundicon-thumb-up:before {
31 | content: "\f000";
32 | }
33 |
34 | .foundicon-thumb-down:before {
35 | content: "\f001";
36 | }
37 |
38 | .foundicon-rss:before {
39 | content: "\f002";
40 | }
41 |
42 | .foundicon-facebook:before {
43 | content: "\f003";
44 | }
45 |
46 | .foundicon-twitter:before {
47 | content: "\f004";
48 | }
49 |
50 | .foundicon-pinterest:before {
51 | content: "\f005";
52 | }
53 |
54 | .foundicon-github:before {
55 | content: "\f006";
56 | }
57 |
58 | .foundicon-path:before {
59 | content: "\f007";
60 | }
61 |
62 | .foundicon-linkedin:before {
63 | content: "\f008";
64 | }
65 |
66 | .foundicon-dribbble:before {
67 | content: "\f009";
68 | }
69 |
70 | .foundicon-stumble-upon:before {
71 | content: "\f00a";
72 | }
73 |
74 | .foundicon-behance:before {
75 | content: "\f00b";
76 | }
77 |
78 | .foundicon-reddit:before {
79 | content: "\f00c";
80 | }
81 |
82 | .foundicon-google-plus:before {
83 | content: "\f00d";
84 | }
85 |
86 | .foundicon-youtube:before {
87 | content: "\f00e";
88 | }
89 |
90 | .foundicon-vimeo:before {
91 | content: "\f00f";
92 | }
93 |
94 | .foundicon-flickr:before {
95 | content: "\f010";
96 | }
97 |
98 | .foundicon-slideshare:before {
99 | content: "\f011";
100 | }
101 |
102 | .foundicon-picassa:before {
103 | content: "\f012";
104 | }
105 |
106 | .foundicon-skype:before {
107 | content: "\f013";
108 | }
109 |
110 | .foundicon-steam:before {
111 | content: "\f014";
112 | }
113 |
114 | .foundicon-instagram:before {
115 | content: "\f015";
116 | }
117 |
118 | .foundicon-foursquare:before {
119 | content: "\f016";
120 | }
121 |
122 | .foundicon-delicious:before {
123 | content: "\f017";
124 | }
125 |
126 | .foundicon-chat:before {
127 | content: "\f018";
128 | }
129 |
130 | .foundicon-torso:before {
131 | content: "\f019";
132 | }
133 |
134 | .foundicon-tumblr:before {
135 | content: "\f01a";
136 | }
137 |
138 | .foundicon-video-chat:before {
139 | content: "\f01b";
140 | }
141 |
142 | .foundicon-digg:before {
143 | content: "\f01c";
144 | }
145 |
146 | .foundicon-wordpress:before {
147 | content: "\f01d";
148 | }
149 |
--------------------------------------------------------------------------------
/docs/documentation/overview/what-is-capistrano/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: What is Capistrano?
3 | layout: default
4 | ---
5 |
6 | ### Capistrano is a remote server automation tool.
7 |
8 | It supports the scripting and execution of arbitrary tasks, and includes a set of sane-default deployment workflows.
9 |
10 | Capistrano can be used to:
11 |
12 | * Reliably deploy web application to any number of machines simultaneously,
13 | in sequence or as a rolling set
14 | * To automate audits of any number of machines (checking login logs,
15 | enumerating uptimes, and/or applying security patches)
16 | * To script arbitrary workflows over SSH
17 | * To automate common tasks in software teams.
18 | * To drive infrastructure provisioning tools such as *chef-solo*, *Ansible* or similar.
19 |
20 | Capistrano is also *very* scriptable, and can be integrated with any other
21 | Ruby software to form part of a larger tool.
22 |
23 | #### What does it look like?
24 |
25 | 
26 |
27 |
28 | #### What else is in the box?
29 |
30 | There's lots of cool stuff in the Capistrano toy box:
31 |
32 | * Interchangeable output formatters (progress, pretty, html, etc)
33 | * Easy to add support for other source control management software.
34 | * A rudimentary multi-console for running Capistrano interactively.
35 | * Host and Role filters for partial deploys, or partial-cluster maintenance.
36 | * Recipes for the Rails asset pipelines, and database migrations.
37 | * Support for complex environments.
38 | * A sane, expressive API:
39 |
40 | ```ruby
41 | desc "Show off the API"
42 | task :ditty do
43 |
44 | on roles(:all) do |host|
45 | # Capture output from the remote host, and re-use it
46 | # we can reflect on the `host` object passed to the block
47 | # and use the `info` logger method to benefit from the
48 | # output formatter that is selected.
49 | uptime = capture('uptime')
50 | if host.roles.include?(:web)
51 | info "Your webserver #{host} has uptime: #{uptime}"
52 | end
53 | end
54 |
55 | on roles(:app) do
56 | # We can set environmental variables for the duration of a block
57 | # and move the process into a directory, executing arbitrary tasks
58 | # such as letting Rails do some heavy lifting.
59 | with({:rails_env => :production}) do
60 | within('/var/www/my/rails/app') do
61 | execute :rails, :runner, 'MyModel.something'
62 | end
63 | end
64 | end
65 |
66 | on roles(:db) do
67 | # We can even switch users, provided we have support on the remote
68 | # server for switching to that user without being prompted for a
69 | # passphrase.
70 | as 'postgres' do
71 | widgets = capture "echo 'SELECT * FROM widgets;' | psql my_database"
72 | if widgets.to_i < 50
73 | warn "There are fewer than 50 widgets in the database on #{host}!"
74 | end
75 | end
76 | end
77 |
78 | on roles(:all) do
79 | # We can even use `test` the way the Unix gods intended
80 | if test("[ -d /some/directory ]")
81 | info "Phew, it's ok, the directory exists!"
82 | end
83 | end
84 | end
85 | ```
86 |
--------------------------------------------------------------------------------
/spec/lib/capistrano/doctor/variables_doctor_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "capistrano/doctor/variables_doctor"
3 |
4 | module Capistrano
5 | module Doctor
6 | describe VariablesDoctor do
7 | include Capistrano::DSL
8 |
9 | let(:doc) { VariablesDoctor.new }
10 |
11 | before do
12 | set :branch, "master"
13 | set :pty, false
14 |
15 | env.variables.untrusted! do
16 | set :application, "my_app"
17 | set :repo_tree, "public"
18 | set :repo_url, ".git"
19 | set :copy_strategy, :scp
20 | set :custom_setting, "hello"
21 | set "string_setting", "hello"
22 | ask :secret
23 | end
24 |
25 | fetch :custom_setting
26 | end
27 |
28 | after { Capistrano::Configuration.reset! }
29 |
30 | it "prints using 4-space indentation" do
31 | expect { doc.call }.to output(/^ {4}/).to_stdout
32 | end
33 |
34 | it "prints variable names and values" do
35 | expect { doc.call }.to output(/:branch\s+"master"$/).to_stdout
36 | expect { doc.call }.to output(/:pty\s+false$/).to_stdout
37 | expect { doc.call }.to output(/:application\s+"my_app"$/).to_stdout
38 | expect { doc.call }.to output(/:repo_url\s+".git"$/).to_stdout
39 | expect { doc.call }.to output(/:repo_tree\s+"public"$/).to_stdout
40 | expect { doc.call }.to output(/:copy_strategy\s+:scp$/).to_stdout
41 | expect { doc.call }.to output(/:custom_setting\s+"hello"$/).to_stdout
42 | expect { doc.call }.to output(/"string_setting"\s+"hello"$/).to_stdout
43 | end
44 |
45 | it "prints unanswered question variable as " do
46 | expect { doc.call }.to output(/:secret\s+$/).to_stdout
47 | end
48 |
49 | it "prints warning for unrecognized variable" do
50 | expect { doc.call }.to \
51 | output(/:copy_strategy is not a recognized Capistrano setting/)\
52 | .to_stdout
53 | end
54 |
55 | it "does not print warning for unrecognized variable that is fetched" do
56 | expect { doc.call }.not_to \
57 | output(/:custom_setting is not a recognized Capistrano setting/)\
58 | .to_stdout
59 | end
60 |
61 | it "does not print warning for whitelisted variable" do
62 | expect { doc.call }.not_to \
63 | output(/:repo_tree is not a recognized Capistrano setting/)\
64 | .to_stdout
65 | end
66 |
67 | describe "Rake" do
68 | before do
69 | load File.expand_path("../../../../../lib/capistrano/doctor.rb",
70 | __FILE__)
71 | end
72 |
73 | after do
74 | Rake::Task.clear
75 | end
76 |
77 | it "has an doctor:variables task that calls VariablesDoctor", capture_io: true do
78 | VariablesDoctor.any_instance.expects(:call)
79 | Rake::Task["doctor:variables"].invoke
80 | end
81 |
82 | it "has a doctor task that depends on doctor:variables" do
83 | expect(Rake::Task["doctor"].prerequisites).to \
84 | include("doctor:variables")
85 | end
86 | end
87 | end
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/docs/documentation/getting-started/flow/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: Flow
3 | layout: default
4 | ---
5 |
6 | Capistrano v3 provides a default **deploy flow** and a **rollback flow**:
7 |
8 | ### Deploy flow
9 |
10 | When you run `cap production deploy`, it invokes the following tasks in
11 | sequence:
12 |
13 | ```ruby
14 | deploy:starting - start a deployment, make sure everything is ready
15 | deploy:started - started hook (for custom tasks)
16 | deploy:updating - update server(s) with a new release
17 | deploy:updated - updated hook
18 | deploy:publishing - publish the new release
19 | deploy:published - published hook
20 | deploy:finishing - finish the deployment, clean up everything
21 | deploy:finished - finished hook
22 | ```
23 |
24 | Notice there are several hook tasks e.g. `:started`, `:updated` for
25 | you to hook up custom tasks into the flow using `after()` and `before()`.
26 |
27 | ### Rollback flow
28 |
29 | When you run `cap production deploy:rollback`, it invokes the following
30 | tasks in sequence:
31 |
32 | ```ruby
33 | deploy:starting
34 | deploy:started
35 | deploy:reverting - revert server(s) to previous release
36 | deploy:reverted - reverted hook
37 | deploy:publishing
38 | deploy:published
39 | deploy:finishing_rollback - finish the rollback, clean up everything
40 | deploy:finished
41 | ```
42 |
43 | As you can see, rollback flow shares many tasks with deploy flow. But note
44 | that, rollback flow runs its own `:finishing_rollback` task because its
45 | cleanup process is usually different from deploy flow.
46 |
47 | ### Flow examples
48 |
49 | Assume you require the following files in `Capfile`,
50 |
51 | ```ruby
52 | # Capfile
53 | require 'capistrano/setup'
54 | require 'capistrano/deploy'
55 | require 'capistrano/bundler'
56 | require 'capistrano/rails/assets'
57 | require 'capistrano/rails/migrations'
58 | ```
59 |
60 | When you run `cap production deploy`, it runs these tasks:
61 |
62 | ```ruby
63 | deploy
64 | deploy:starting
65 | [before]
66 | deploy:ensure_stage
67 | deploy:set_shared_assets
68 | deploy:check
69 | deploy:started
70 | deploy:updating
71 | git:create_release
72 | deploy:symlink:shared
73 | deploy:updated
74 | [before]
75 | deploy:bundle
76 | [after]
77 | deploy:migrate
78 | deploy:compile_assets
79 | deploy:normalize_assets
80 | deploy:publishing
81 | deploy:symlink:release
82 | deploy:published
83 | deploy:finishing
84 | deploy:cleanup
85 | deploy:finished
86 | deploy:log_revision
87 | ```
88 |
89 | For `cap production deploy:rollback`, it runs these tasks:
90 |
91 | ```ruby
92 | deploy
93 | deploy:starting
94 | [before]
95 | deploy:ensure_stage
96 | deploy:set_shared_assets
97 | deploy:check
98 | deploy:started
99 | deploy:reverting
100 | deploy:revert_release
101 | deploy:reverted
102 | [after]
103 | deploy:rollback_assets
104 | deploy:publishing
105 | deploy:symlink:release
106 | deploy:published
107 | deploy:finishing_rollback
108 | deploy:cleanup_rollback
109 | deploy:finished
110 | deploy:log_revision
111 | ```
112 |
--------------------------------------------------------------------------------
/lib/capistrano/plugin.rb:
--------------------------------------------------------------------------------
1 | require "capistrano/all"
2 | require "rake/tasklib"
3 |
4 | # IMPORTANT: The Capistrano::Plugin system is not yet considered a stable,
5 | # public API, and is subject to change without notice. Eventually it will be
6 | # officially documented and supported, but for now, use it at your own risk.
7 | #
8 | # Base class for Capistrano plugins. Makes building a Capistrano plugin as easy
9 | # as writing a `Capistrano::Plugin` subclass and overriding any or all of its
10 | # three template methods:
11 | #
12 | # * set_defaults
13 | # * register_hooks
14 | # * define_tasks
15 | #
16 | # Within the plugin you can use any methods of the Rake or Capistrano DSLs, like
17 | # `fetch`, `invoke`, etc. In cases when you need to use SSHKit's backend outside
18 | # of an `on` block, use the `backend` convenience method. E.g. `backend.test`,
19 | # `backend.execute`, or `backend.capture`.
20 | #
21 | # Package up and distribute your plugin class as a gem and you're good to go!
22 | #
23 | # To use a plugin, all a user has to do is install it in the Capfile, like this:
24 | #
25 | # # Capfile
26 | # require "capistrano/superfancy"
27 | # install_plugin Capistrano::Superfancy
28 | #
29 | # Or, to install the plugin without its hooks:
30 | #
31 | # # Capfile
32 | # require "capistrano/superfancy"
33 | # install_plugin Capistrano::Superfancy, load_hooks: false
34 | #
35 | class Capistrano::Plugin < Rake::TaskLib
36 | include Capistrano::DSL
37 |
38 | # Implemented by subclasses to provide default values for settings needed by
39 | # this plugin. Typically done using the `set_if_empty` Capistrano DSL method.
40 | #
41 | # Example:
42 | #
43 | # def set_defaults
44 | # set_if_empty :my_plugin_option, true
45 | # end
46 | #
47 | def set_defaults; end
48 |
49 | # Implemented by subclasses to hook into Capistrano's deployment flow using
50 | # using the `before` and `after` DSL methods. Note that `register_hooks` will
51 | # not be called if the user has opted-out of hooks when installing the plugin.
52 | #
53 | # Example:
54 | #
55 | # def register_hooks
56 | # after "deploy:updated", "my_plugin:do_something"
57 | # end
58 | #
59 | def register_hooks; end
60 |
61 | # Implemented by subclasses to define Rake tasks. Typically a plugin will call
62 | # `eval_rakefile` to load Rake tasks from a separate .rake file.
63 | #
64 | # Example:
65 | #
66 | # def define_tasks
67 | # eval_rakefile File.expand_path("../tasks.rake", __FILE__)
68 | # end
69 | #
70 | # For simple tasks, you can define them inline. No need for a separate file.
71 | #
72 | # def define_tasks
73 | # desc "Do something fantastic."
74 | # task "my_plugin:fantastic" do
75 | # ...
76 | # end
77 | # end
78 | #
79 | def define_tasks; end
80 |
81 | private
82 |
83 | # Read and eval a .rake file in such a way that `self` within the .rake file
84 | # refers to this plugin instance. This gives the tasks in the file access to
85 | # helper methods defined by the plugin.
86 | def eval_rakefile(path)
87 | contents = IO.read(path)
88 | instance_eval(contents, path, 1)
89 | end
90 |
91 | # Convenience to access the current SSHKit backend outside of an `on` block.
92 | def backend
93 | SSHKit::Backend.current
94 | end
95 | end
96 |
--------------------------------------------------------------------------------
/docs/documentation/advanced-features/filtering/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: Filtering
3 | layout: default
4 | ---
5 |
6 | Filtering is the term given to reducing the entire set of servers declared in a stage file
7 | to a smaller set. There are three types of filters used in Capistrano (Host, Role and
8 | Property) and they take effect in two quite different ways because of the two distinct
9 | uses to which the declarations of servers, roles and properties are put in tasks:
10 |
11 | * To determine _configurations_: typically by using the `roles()`, `release_roles()` and
12 | `primary()` methods. Typically these are used outside the scope of the `on()` method.
13 |
14 | * To _interact_ with remote hosts using the `on()` method
15 |
16 | An illustration of this would be to create a `/etc/krb5.conf` file containing the list of
17 | available KDC's by using the list of servers returned by `roles(:kdc)` and then uploading
18 | it to all client machines using `on(roles(:all)) do upload!(file) end`
19 |
20 | A problem with this arises when _filters_ are used. Filters are designed to limit the
21 | actual set of hosts that are used to a subset of those in the overall stage, but how
22 | should that apply in the above case?
23 |
24 | If the filter applies to both the _interaction_ and _configuration_ aspects, any configuration
25 | files deployed will not be the same as those on the hosts excluded by the filters. This is
26 | almost certainly not what is wanted, the filters should apply only to the _interactions_
27 | ensuring that any configuration files deployed will be identical across the stage.
28 |
29 | So we define two different categories of filter, the interaction ones which are called _On-Filters_
30 | and the configuration ones which are _Property-Filters_
31 |
32 | ### On-Filtering
33 |
34 | On-filters apply only to the `on()` method that invokes SSH. There are two default types:
35 |
36 | * [Host Filters](/documentation/advanced-features/host-filtering/), and
37 |
38 | * [Role Filters](/documentation/advanced-features/role-filtering/)
39 |
40 |
41 | In both the above cases, when filters are specified using comma separated lists, the final
42 | filter is the _union_ of all of the components. However when multiple filters are declared
43 | the result is the _intersection_.
44 |
45 | This means that you can filter by both role and host but you will get the _intersection_
46 | of the servers. For example, lets say you filtered by the role `app`, then by
47 | the hostnames `server1` and `server2`. Capistrano would first filter the
48 | available servers to only those with the role `app`, then filter them
49 | to look for servers with the hostname `server1` or `server2`. If only `server2`
50 | had the role `app` (`server1` has some other role), then in this situation your
51 | task would only run on `server2`.
52 |
53 | Custom filters may also be added; see
54 | [Custom Filters](/documentation/advanced-features/custom-filters/).
55 |
56 | ### Property-Filtering
57 |
58 | Property-filters select servers based on the value of their properties alone and
59 | are specified by options passed to the `roles()` method (and implicitly in methods
60 | like `release_roles()` and `primary()`)
61 |
62 | An example of that is the 'no_release' property and it's use in the `release_roles()` method.
63 |
64 | See the [documentation](/documentation/advanced-features/property-filtering/) for
65 | details
66 |
--------------------------------------------------------------------------------
/docs/documentation/advanced-features/custom-filters/index.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | title: Custom Filters
3 | layout: default
4 | ---
5 |
6 | Custom filters (specifically, Custom On-Filters) limit the hosts that are being
7 | deployed to, in the same way as
8 | [Host](/documentation/advanced-features/host-filtering/) and
9 | [Role](/documentation/advanced-features/role-filtering/) filters, but the exact
10 | method used to filter servers is up to the user of Capistrano.
11 |
12 | Filters may be added to Capistrano's list of filters by using the
13 | `Configuration#add_filter` method. Filters must respond to a `filter` method,
14 | which will be given an array of Servers, and should return a subset of that
15 | array (the servers which passed the filter).
16 |
17 | `Configuration#add_filter` may also take a block, in which case the block is
18 | expected to be unary. The block will be passed an array of servers, and is
19 | expected to return a subset of that array.
20 |
21 | Either a block or object may be passed to `add_filter`, but not both.
22 |
23 | ### Example
24 |
25 | You may have a large group of servers that are partitioned into separate regions
26 | that correspond to actual geographic regions. Usually, you deploy to all of
27 | them, but there are cases where you want to deploy to a specific region.
28 |
29 | Capistrano recognizes the concept of a server's *role* and *hostname*, but has
30 | no concept of a *region*. In this case, you can construct your own filter that
31 | selects servers based on their region. When defining servers, you may provide
32 | them with a `region` property, and use that property in your filter.
33 |
34 |
35 | The filter could look like this:
36 |
37 | `config/deploy.rb`
38 |
39 | ```ruby
40 | class RegionFilter
41 |
42 | def initialize(regions)
43 | @regions = Array(regions)
44 | end
45 |
46 | def filter(servers)
47 | servers.select {|server|
48 | region = server.fetch(:region)
49 | region && @regions.include?(region)
50 | }
51 | end
52 |
53 | end
54 | ```
55 |
56 | You would add servers like this:
57 |
58 | `config/deploy/production.rb`
59 |
60 | ```ruby
61 | server('123.123.123.123', region: 'north-east')
62 | server('12.12.12.12', region: 'south-west')
63 | server('4.5.6.7', region: 'mid-west')
64 | ```
65 |
66 | To tell Capistrano to use this filter, you would use the
67 | `Configuration#add_filter` method. In this example, we look at the `REGIONS`
68 | environment variable, and take it to be a comma-separated list of regions that
69 | we're interested in:
70 |
71 | `config/deploy.rb`
72 |
73 | ```ruby
74 | if ENV['REGIONS']
75 | regions = ENV['REGIONS'].split(',')
76 | filter = RegionFilter.new(regions)
77 | Capistrano::Configuration.env.add_filter(filter)
78 | end
79 | ```
80 |
81 | We obtain a list of regions to deploy to from the environment variable,
82 | construct a new filter with those regions, and add it to Capistrano's list of
83 | filters.
84 |
85 | Of course, we're not limited to regions. Any time you can classify or partition
86 | a list of servers in a way that you only want to deploy to some of them, you can
87 | use a custom filter. For another example, you might arbitrarily assign your
88 | servers to either an *A* group or a *B* group, and deploy a new version only to
89 | the *B* group as a simple variant of A/B Testing.
90 |
--------------------------------------------------------------------------------
/lib/capistrano/scm.rb:
--------------------------------------------------------------------------------
1 | module Capistrano
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, "Your SCM strategy module should provide a #test method"
62 | end
63 |
64 | # @abstract
65 | #
66 | # Your implementation should check if the specified remote-repository is
67 | # available.
68 | #
69 | # @return [Boolean]
70 | #
71 | def check
72 | raise NotImplementedError, "Your SCM strategy module should provide a #check method"
73 | end
74 |
75 | # @abstract
76 | #
77 | # Create a (new) clone of the remote-repository on the deployment target
78 | #
79 | # @return void
80 | #
81 | def clone
82 | raise NotImplementedError, "Your SCM strategy module should provide a #clone method"
83 | end
84 |
85 | # @abstract
86 | #
87 | # Update the clone on the deployment target
88 | #
89 | # @return void
90 | #
91 | def update
92 | raise NotImplementedError, "Your SCM strategy module should provide a #update method"
93 | end
94 |
95 | # @abstract
96 | #
97 | # Copy the contents of the cache-repository onto the release path
98 | #
99 | # @return void
100 | #
101 | def release
102 | raise NotImplementedError, "Your SCM strategy module should provide a #release method"
103 | end
104 |
105 | # @abstract
106 | #
107 | # Identify the SHA of the commit that will be deployed. This will most likely involve SshKit's capture method.
108 | #
109 | # @return void
110 | #
111 | def fetch_revision
112 | raise NotImplementedError, "Your SCM strategy module should provide a #fetch_revision method"
113 | end
114 | end
115 | end
116 |
--------------------------------------------------------------------------------
/lib/capistrano/scm/tasks/git.rake:
--------------------------------------------------------------------------------
1 | # This trick lets us access the Git plugin within `on` blocks.
2 | git_plugin = self
3 |
4 | namespace :git do
5 | desc "Upload the git wrapper script, this script guarantees that we can script git without getting an interactive prompt"
6 | task :wrapper do
7 | on release_roles(:all), in: :groups, limit: fetch(:git_max_concurrent_connections), wait: fetch(:git_wait_interval) do
8 | execute :mkdir, "-p", File.dirname(fetch(:git_wrapper_path)).shellescape
9 | upload! StringIO.new("#!/bin/sh -e\nexec /usr/bin/env ssh -o PasswordAuthentication=no -o StrictHostKeyChecking=no \"$@\"\n"), fetch(:git_wrapper_path)
10 | execute :chmod, "700", fetch(:git_wrapper_path).shellescape
11 | end
12 | end
13 |
14 | desc "Check that the repository is reachable"
15 | task check: :'git:wrapper' do
16 | fetch(:branch)
17 | on release_roles(:all), in: :groups, limit: fetch(:git_max_concurrent_connections), wait: fetch(:git_wait_interval) do
18 | with fetch(:git_environmental_variables) do
19 | git_plugin.check_repo_is_reachable
20 | end
21 | end
22 | end
23 |
24 | desc "Clone the repo to the cache"
25 | task clone: :'git:wrapper' do
26 | on release_roles(:all), in: :groups, limit: fetch(:git_max_concurrent_connections), wait: fetch(:git_wait_interval) do
27 | if git_plugin.repo_mirror_exists?
28 | info t(:mirror_exists, at: repo_path)
29 | else
30 | within deploy_path do
31 | with fetch(:git_environmental_variables) do
32 | git_plugin.clone_repo
33 | end
34 | end
35 | end
36 | end
37 | end
38 |
39 | desc "Update the repo mirror to reflect the origin state"
40 | task update: :'git:clone' do
41 | on release_roles(:all), in: :groups, limit: fetch(:git_max_concurrent_connections), wait: fetch(:git_wait_interval) do
42 | within repo_path do
43 | with fetch(:git_environmental_variables) do
44 | git_plugin.update_mirror
45 | git_plugin.verify_commit if fetch(:git_verify_commit)
46 | end
47 | end
48 | end
49 | end
50 |
51 | desc "Copy repo to releases"
52 | task create_release: :'git:update' do
53 | on release_roles(:all), in: :groups, limit: fetch(:git_max_concurrent_connections), wait: fetch(:git_wait_interval) do
54 | with fetch(:git_environmental_variables) do
55 | within repo_path do
56 | execute :mkdir, "-p", release_path
57 | git_plugin.archive_to_release_path
58 | end
59 | end
60 | end
61 | end
62 |
63 | desc "Determine the revision that will be deployed"
64 | task :set_current_revision do
65 | on release_roles(:all), in: :groups, limit: fetch(:git_max_concurrent_connections), wait: fetch(:git_wait_interval) do
66 | within repo_path do
67 | with fetch(:git_environmental_variables) do
68 | set :current_revision, git_plugin.fetch_revision
69 | end
70 | end
71 | end
72 | end
73 |
74 | desc "Determine the unix timestamp that the revision that will be deployed was created"
75 | task :set_current_revision_time do
76 | on release_roles(:all), in: :groups, limit: fetch(:git_max_concurrent_connections), wait: fetch(:git_wait_interval) do
77 | within repo_path do
78 | with fetch(:git_environmental_variables) do
79 | set :current_revision_time, git_plugin.fetch_revision_time
80 | end
81 | end
82 | end
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/spec/lib/capistrano/configuration/question_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 |
3 | module Capistrano
4 | class Configuration
5 | describe Question do
6 | let(:question) { Question.new(key, default, stdin: stdin) }
7 | let(:question_without_echo) { Question.new(key, default, echo: false, stdin: stdin) }
8 | let(:question_without_default) { Question.new(key, nil, stdin: stdin) }
9 | let(:question_prompt) { Question.new(key, default, stdin: stdin, prompt: "Your favorite branch") }
10 | let(:question_prompt_without_default) { Question.new(key, nil, stdin: stdin, prompt: "Your favorite branch") }
11 | let(:default) { :default }
12 | let(:key) { :branch }
13 | let(:stdin) { stub(tty?: true) }
14 |
15 | describe ".new" do
16 | it "takes a key, default, options" do
17 | question
18 | end
19 | end
20 |
21 | describe "#call" do
22 | context "value is entered" do
23 | let(:branch) { "branch" }
24 |
25 | it "returns the echoed value" do
26 | $stdout.expects(:print).with("Please enter branch (default): ")
27 | stdin.expects(:gets).returns(branch)
28 | stdin.expects(:noecho).never
29 |
30 | expect(question.call).to eq(branch)
31 | end
32 |
33 | it "returns the value but does not echo it" do
34 | $stdout.expects(:print).with("Please enter branch (default): ")
35 | stdin.expects(:noecho).returns(branch)
36 | $stdout.expects(:print).with("\n")
37 |
38 | expect(question_without_echo.call).to eq(branch)
39 | end
40 |
41 | it "returns the value but has no default between parenthesis" do
42 | $stdout.expects(:print).with("Please enter branch: ")
43 | stdin.expects(:gets).returns(branch)
44 | stdin.expects(:noecho).never
45 |
46 | expect(question_without_default.call).to eq(branch)
47 | end
48 |
49 | it "uses prompt and returns the value" do
50 | $stdout.expects(:print).with("Your favorite branch (default): ")
51 | stdin.expects(:gets).returns(branch)
52 | stdin.expects(:noecho).never
53 |
54 | expect(question_prompt.call).to eq(branch)
55 | end
56 |
57 | it "uses prompt and returns the value but has no default between parenthesis" do
58 | $stdout.expects(:print).with("Your favorite branch: ")
59 | stdin.expects(:gets).returns(branch)
60 | stdin.expects(:noecho).never
61 |
62 | expect(question_prompt_without_default.call).to eq(branch)
63 | end
64 | end
65 |
66 | context "value is not entered" do
67 | let(:branch) { default }
68 |
69 | before do
70 | $stdout.expects(:print).with("Please enter branch (default): ")
71 | stdin.expects(:gets).returns("")
72 | end
73 |
74 | it "returns the default as the value" do
75 | expect(question.call).to eq(branch)
76 | end
77 | end
78 |
79 | context "tty unavailable", capture_io: true do
80 | before do
81 | stdin.expects(:gets).never
82 | stdin.expects(:tty?).returns(false)
83 | end
84 |
85 | it "returns the default as the value" do
86 | expect(question.call).to eq(default)
87 | end
88 | end
89 | end
90 | end
91 | end
92 | end
93 |
--------------------------------------------------------------------------------