capistrano 2.8.0 → 3.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.docker/Dockerfile +7 -0
- data/.docker/ssh_key_rsa +49 -0
- data/.docker/ssh_key_rsa.pub +1 -0
- data/.docker/ubuntu_setup.sh +23 -0
- data/.github/issue_template.md +19 -0
- data/.github/pull_request_template.md +22 -0
- data/.github/release-drafter.yml +25 -0
- data/.github/workflows/ci.yml +80 -0
- data/.github/workflows/release-drafter.yml +18 -0
- data/.gitignore +23 -8
- data/.rubocop.yml +62 -0
- data/CHANGELOG.md +1 -0
- data/CONTRIBUTING.md +63 -0
- data/DEVELOPMENT.md +112 -0
- data/Gemfile +42 -9
- data/LICENSE.txt +21 -0
- data/README.md +221 -0
- data/RELEASING.md +17 -0
- data/Rakefile +17 -8
- data/UPGRADING-3.7.md +86 -0
- data/bin/cap +2 -3
- data/bin/capify +7 -89
- data/capistrano.gemspec +29 -43
- data/docker-compose.yml +8 -0
- data/features/configuration.feature +28 -0
- data/features/deploy.feature +92 -0
- data/features/deploy_failure.feature +17 -0
- data/features/doctor.feature +11 -0
- data/features/installation.feature +21 -0
- data/features/sshconnect.feature +11 -0
- data/features/stage_failure.feature +9 -0
- data/features/step_definitions/assertions.rb +162 -0
- data/features/step_definitions/cap_commands.rb +21 -0
- data/features/step_definitions/setup.rb +91 -0
- data/features/subdirectory.feature +9 -0
- data/features/support/docker_gateway.rb +53 -0
- data/features/support/env.rb +1 -0
- data/features/support/remote_command_helpers.rb +29 -0
- data/features/support/remote_ssh_helpers.rb +33 -0
- data/lib/Capfile +3 -0
- data/lib/capistrano/all.rb +17 -0
- data/lib/capistrano/application.rb +153 -0
- data/lib/capistrano/configuration/empty_filter.rb +9 -0
- data/lib/capistrano/configuration/filter.rb +26 -0
- data/lib/capistrano/configuration/host_filter.rb +29 -0
- data/lib/capistrano/configuration/null_filter.rb +9 -0
- data/lib/capistrano/configuration/plugin_installer.rb +51 -0
- data/lib/capistrano/configuration/question.rb +76 -0
- data/lib/capistrano/configuration/role_filter.rb +29 -0
- data/lib/capistrano/configuration/scm_resolver.rb +149 -0
- data/lib/capistrano/configuration/server.rb +137 -0
- data/lib/capistrano/configuration/servers.rb +56 -96
- data/lib/capistrano/configuration/validated_variables.rb +110 -0
- data/lib/capistrano/configuration/variables.rb +79 -94
- data/lib/capistrano/configuration.rb +178 -33
- data/lib/capistrano/console.rb +1 -0
- data/lib/capistrano/defaults.rb +36 -0
- data/lib/capistrano/deploy.rb +3 -0
- data/lib/capistrano/doctor/environment_doctor.rb +19 -0
- data/lib/capistrano/doctor/gems_doctor.rb +45 -0
- data/lib/capistrano/doctor/output_helpers.rb +79 -0
- data/lib/capistrano/doctor/servers_doctor.rb +105 -0
- data/lib/capistrano/doctor/variables_doctor.rb +74 -0
- data/lib/capistrano/doctor.rb +6 -0
- data/lib/capistrano/dotfile.rb +2 -0
- data/lib/capistrano/dsl/env.rb +43 -0
- data/lib/capistrano/dsl/paths.rb +89 -0
- data/lib/capistrano/dsl/stages.rb +31 -0
- data/lib/capistrano/dsl/task_enhancements.rb +61 -0
- data/lib/capistrano/dsl.rb +95 -0
- data/lib/capistrano/framework.rb +2 -0
- data/lib/capistrano/i18n.rb +46 -0
- data/lib/capistrano/immutable_task.rb +30 -0
- data/lib/capistrano/install.rb +1 -0
- data/lib/capistrano/plugin.rb +95 -0
- data/lib/capistrano/proc_helpers.rb +13 -0
- data/lib/capistrano/scm/git.rb +105 -0
- data/lib/capistrano/scm/hg.rb +55 -0
- data/lib/capistrano/scm/plugin.rb +13 -0
- data/lib/capistrano/scm/svn.rb +56 -0
- data/lib/capistrano/scm/tasks/git.rake +84 -0
- data/lib/capistrano/scm/tasks/hg.rake +53 -0
- data/lib/capistrano/scm/tasks/svn.rake +53 -0
- data/lib/capistrano/scm.rb +115 -0
- data/lib/capistrano/setup.rb +36 -0
- data/lib/capistrano/tasks/console.rake +25 -0
- data/lib/capistrano/tasks/deploy.rake +280 -0
- data/lib/capistrano/tasks/doctor.rake +24 -0
- data/lib/capistrano/tasks/framework.rake +67 -0
- data/lib/capistrano/tasks/install.rake +41 -0
- data/lib/capistrano/templates/Capfile +38 -0
- data/lib/capistrano/templates/deploy.rb.erb +39 -0
- data/lib/capistrano/templates/stage.rb.erb +61 -0
- data/lib/capistrano/upload_task.rb +9 -0
- data/lib/capistrano/version.rb +1 -14
- data/lib/capistrano/version_validator.rb +32 -0
- data/lib/capistrano.rb +0 -3
- data/spec/integration/dsl_spec.rb +632 -0
- data/spec/integration_spec_helper.rb +5 -0
- data/spec/lib/capistrano/application_spec.rb +60 -0
- data/spec/lib/capistrano/configuration/empty_filter_spec.rb +17 -0
- data/spec/lib/capistrano/configuration/filter_spec.rb +109 -0
- data/spec/lib/capistrano/configuration/host_filter_spec.rb +71 -0
- data/spec/lib/capistrano/configuration/null_filter_spec.rb +17 -0
- data/spec/lib/capistrano/configuration/plugin_installer_spec.rb +98 -0
- data/spec/lib/capistrano/configuration/question_spec.rb +92 -0
- data/spec/lib/capistrano/configuration/role_filter_spec.rb +80 -0
- data/spec/lib/capistrano/configuration/scm_resolver_spec.rb +56 -0
- data/spec/lib/capistrano/configuration/server_spec.rb +309 -0
- data/spec/lib/capistrano/configuration/servers_spec.rb +331 -0
- data/spec/lib/capistrano/configuration_spec.rb +357 -0
- data/spec/lib/capistrano/doctor/environment_doctor_spec.rb +44 -0
- data/spec/lib/capistrano/doctor/gems_doctor_spec.rb +67 -0
- data/spec/lib/capistrano/doctor/output_helpers_spec.rb +47 -0
- data/spec/lib/capistrano/doctor/servers_doctor_spec.rb +86 -0
- data/spec/lib/capistrano/doctor/variables_doctor_spec.rb +89 -0
- data/spec/lib/capistrano/dsl/paths_spec.rb +228 -0
- data/spec/lib/capistrano/dsl/task_enhancements_spec.rb +108 -0
- data/spec/lib/capistrano/dsl_spec.rb +125 -0
- data/spec/lib/capistrano/immutable_task_spec.rb +31 -0
- data/spec/lib/capistrano/plugin_spec.rb +84 -0
- data/spec/lib/capistrano/scm/git_spec.rb +194 -0
- data/spec/lib/capistrano/scm/hg_spec.rb +109 -0
- data/spec/lib/capistrano/scm/svn_spec.rb +137 -0
- data/spec/lib/capistrano/scm_spec.rb +103 -0
- data/spec/lib/capistrano/upload_task_spec.rb +19 -0
- data/spec/lib/capistrano/version_validator_spec.rb +118 -0
- data/spec/lib/capistrano_spec.rb +7 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/support/matchers.rb +5 -0
- data/spec/support/tasks/database.rake +11 -0
- data/spec/support/tasks/fail.rake +8 -0
- data/spec/support/tasks/failed.rake +5 -0
- data/spec/support/tasks/plugin.rake +6 -0
- data/spec/support/tasks/root.rake +11 -0
- data/spec/support/test_app.rb +205 -0
- metadata +234 -208
- data/.rvmrc +0 -1
- data/CHANGELOG +0 -954
- data/README.mdown +0 -76
- data/lib/capistrano/callback.rb +0 -45
- data/lib/capistrano/cli/execute.rb +0 -85
- data/lib/capistrano/cli/help.rb +0 -125
- data/lib/capistrano/cli/help.txt +0 -81
- data/lib/capistrano/cli/options.rb +0 -243
- data/lib/capistrano/cli/ui.rb +0 -40
- data/lib/capistrano/cli.rb +0 -47
- data/lib/capistrano/command.rb +0 -286
- data/lib/capistrano/configuration/actions/file_transfer.rb +0 -51
- data/lib/capistrano/configuration/actions/inspect.rb +0 -46
- data/lib/capistrano/configuration/actions/invocation.rb +0 -298
- data/lib/capistrano/configuration/callbacks.rb +0 -148
- data/lib/capistrano/configuration/connections.rb +0 -230
- data/lib/capistrano/configuration/execution.rb +0 -143
- data/lib/capistrano/configuration/loading.rb +0 -197
- data/lib/capistrano/configuration/namespaces.rb +0 -197
- data/lib/capistrano/configuration/roles.rb +0 -73
- data/lib/capistrano/errors.rb +0 -19
- data/lib/capistrano/ext/string.rb +0 -5
- data/lib/capistrano/extensions.rb +0 -57
- data/lib/capistrano/logger.rb +0 -59
- data/lib/capistrano/processable.rb +0 -53
- data/lib/capistrano/recipes/compat.rb +0 -32
- data/lib/capistrano/recipes/deploy/assets.rb +0 -57
- data/lib/capistrano/recipes/deploy/dependencies.rb +0 -44
- data/lib/capistrano/recipes/deploy/local_dependency.rb +0 -54
- data/lib/capistrano/recipes/deploy/remote_dependency.rb +0 -111
- data/lib/capistrano/recipes/deploy/scm/accurev.rb +0 -169
- data/lib/capistrano/recipes/deploy/scm/base.rb +0 -196
- data/lib/capistrano/recipes/deploy/scm/bzr.rb +0 -86
- data/lib/capistrano/recipes/deploy/scm/cvs.rb +0 -153
- data/lib/capistrano/recipes/deploy/scm/darcs.rb +0 -96
- data/lib/capistrano/recipes/deploy/scm/git.rb +0 -282
- data/lib/capistrano/recipes/deploy/scm/mercurial.rb +0 -137
- data/lib/capistrano/recipes/deploy/scm/none.rb +0 -44
- data/lib/capistrano/recipes/deploy/scm/perforce.rb +0 -138
- data/lib/capistrano/recipes/deploy/scm/subversion.rb +0 -121
- data/lib/capistrano/recipes/deploy/scm.rb +0 -19
- data/lib/capistrano/recipes/deploy/strategy/base.rb +0 -88
- data/lib/capistrano/recipes/deploy/strategy/checkout.rb +0 -20
- data/lib/capistrano/recipes/deploy/strategy/copy.rb +0 -224
- data/lib/capistrano/recipes/deploy/strategy/export.rb +0 -20
- data/lib/capistrano/recipes/deploy/strategy/remote.rb +0 -52
- data/lib/capistrano/recipes/deploy/strategy/remote_cache.rb +0 -57
- data/lib/capistrano/recipes/deploy/strategy.rb +0 -19
- data/lib/capistrano/recipes/deploy/templates/maintenance.rhtml +0 -53
- data/lib/capistrano/recipes/deploy.rb +0 -568
- data/lib/capistrano/recipes/standard.rb +0 -37
- data/lib/capistrano/recipes/templates/maintenance.rhtml +0 -53
- data/lib/capistrano/role.rb +0 -102
- data/lib/capistrano/server_definition.rb +0 -56
- data/lib/capistrano/shell.rb +0 -260
- data/lib/capistrano/ssh.rb +0 -101
- data/lib/capistrano/task_definition.rb +0 -75
- data/lib/capistrano/transfer.rb +0 -216
- data/rvmrc.sample +0 -1
- data/test/cli/execute_test.rb +0 -132
- data/test/cli/help_test.rb +0 -165
- data/test/cli/options_test.rb +0 -329
- data/test/cli/ui_test.rb +0 -28
- data/test/cli_test.rb +0 -17
- data/test/command_test.rb +0 -289
- data/test/configuration/actions/file_transfer_test.rb +0 -61
- data/test/configuration/actions/inspect_test.rb +0 -65
- data/test/configuration/actions/invocation_test.rb +0 -247
- data/test/configuration/callbacks_test.rb +0 -220
- data/test/configuration/connections_test.rb +0 -420
- data/test/configuration/execution_test.rb +0 -175
- data/test/configuration/loading_test.rb +0 -132
- data/test/configuration/namespace_dsl_test.rb +0 -311
- data/test/configuration/roles_test.rb +0 -144
- data/test/configuration/servers_test.rb +0 -183
- data/test/configuration/variables_test.rb +0 -190
- data/test/configuration_test.rb +0 -88
- data/test/deploy/local_dependency_test.rb +0 -76
- data/test/deploy/remote_dependency_test.rb +0 -135
- data/test/deploy/scm/accurev_test.rb +0 -23
- data/test/deploy/scm/base_test.rb +0 -55
- data/test/deploy/scm/bzr_test.rb +0 -51
- data/test/deploy/scm/darcs_test.rb +0 -37
- data/test/deploy/scm/git_test.rb +0 -184
- data/test/deploy/scm/mercurial_test.rb +0 -134
- data/test/deploy/scm/none_test.rb +0 -35
- data/test/deploy/scm/subversion_test.rb +0 -32
- data/test/deploy/strategy/copy_test.rb +0 -321
- data/test/extensions_test.rb +0 -69
- data/test/fixtures/cli_integration.rb +0 -5
- data/test/fixtures/config.rb +0 -5
- data/test/fixtures/custom.rb +0 -3
- data/test/logger_test.rb +0 -123
- data/test/recipes_test.rb +0 -25
- data/test/role_test.rb +0 -11
- data/test/server_definition_test.rb +0 -121
- data/test/shell_test.rb +0 -90
- data/test/ssh_test.rb +0 -113
- data/test/task_definition_test.rb +0 -116
- data/test/transfer_test.rb +0 -160
- data/test/utils.rb +0 -37
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Feature: Deploy failure
|
|
2
|
+
|
|
3
|
+
Background:
|
|
4
|
+
Given a test app with the default configuration
|
|
5
|
+
And a custom task that will simulate a failure
|
|
6
|
+
And a custom task to run in the event of a failure
|
|
7
|
+
And servers with the roles app and web
|
|
8
|
+
|
|
9
|
+
Scenario: Triggering the custom task
|
|
10
|
+
When I run cap "deploy:starting"
|
|
11
|
+
But an error is raised
|
|
12
|
+
Then the failure task will not run
|
|
13
|
+
|
|
14
|
+
Scenario: Triggering the custom task
|
|
15
|
+
When I run cap "deploy"
|
|
16
|
+
But an error is raised
|
|
17
|
+
Then the failure task will run
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Feature: Doctor
|
|
2
|
+
|
|
3
|
+
Background:
|
|
4
|
+
Given a test app with the default configuration
|
|
5
|
+
|
|
6
|
+
Scenario: Running the doctor task
|
|
7
|
+
When I run cap "doctor"
|
|
8
|
+
Then the task is successful
|
|
9
|
+
And contains "Environment" in the output
|
|
10
|
+
And contains "Gems" in the output
|
|
11
|
+
And contains "Variables" in the output
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Feature: Installation
|
|
2
|
+
|
|
3
|
+
Background:
|
|
4
|
+
Given a test app without any configuration
|
|
5
|
+
|
|
6
|
+
Scenario: The "install" task documentation can be viewed
|
|
7
|
+
When I run "cap -T"
|
|
8
|
+
Then the task is successful
|
|
9
|
+
And contains "cap install" in the output
|
|
10
|
+
|
|
11
|
+
Scenario: With default stages
|
|
12
|
+
When I run "cap install"
|
|
13
|
+
Then the deploy.rb file is created
|
|
14
|
+
And the default stage files are created
|
|
15
|
+
And the tasks folder is created
|
|
16
|
+
|
|
17
|
+
Scenario: With specified stages
|
|
18
|
+
When I run "cap install STAGES=qa,production"
|
|
19
|
+
Then the deploy.rb file is created
|
|
20
|
+
And the specified stage files are created
|
|
21
|
+
And the tasks folder is created
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Feature: SSH Connection
|
|
2
|
+
|
|
3
|
+
Background:
|
|
4
|
+
Given a test app with the default configuration
|
|
5
|
+
And servers with the roles app and web
|
|
6
|
+
And a task which executes as root
|
|
7
|
+
|
|
8
|
+
Scenario: Switching from default user to root and back again
|
|
9
|
+
When I run cap "am_i_root"
|
|
10
|
+
Then the task is successful
|
|
11
|
+
And the output matches "I am uid=0\(root\)" followed by "I am uid=\d+\(deployer\)"
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
require "shellwords"
|
|
2
|
+
|
|
3
|
+
Then(/^references in the remote repo are listed$/) do
|
|
4
|
+
expect(@output).to include("refs/heads/master")
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
Then(/^git wrapper permissions are 0700$/) do
|
|
8
|
+
permissions_test = %Q([ $(stat -c "%a" #{TestApp.git_wrapper_path_glob}) == "700" ])
|
|
9
|
+
expect { run_remote_ssh_command(permissions_test) }.not_to raise_error
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
Then(/^the shared path is created$/) do
|
|
13
|
+
run_remote_ssh_command(test_dir_exists(TestApp.shared_path))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
Then(/^the releases path is created$/) do
|
|
17
|
+
run_remote_ssh_command(test_dir_exists(TestApp.releases_path))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
Then(/^(\d+) valid releases are kept/) do |num|
|
|
21
|
+
test = %Q([ $(ls -g #{TestApp.releases_path} | grep -E '[0-9]{14}' | wc -l) == "#{num}" ])
|
|
22
|
+
expect { run_remote_ssh_command(test) }.not_to raise_error
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
Then(/^the invalid (.+) release is ignored$/) do |filename|
|
|
26
|
+
test = "ls -g #{TestApp.releases_path} | grep #{filename}"
|
|
27
|
+
expect { run_remote_ssh_command(test) }.not_to raise_error
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
Then(/^directories in :linked_dirs are created in shared$/) do
|
|
31
|
+
TestApp.linked_dirs.each do |dir|
|
|
32
|
+
run_remote_ssh_command(test_dir_exists(TestApp.shared_path.join(dir)))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Then(/^directories referenced in :linked_files are created in shared$/) do
|
|
37
|
+
dirs = TestApp.linked_files.map { |path| TestApp.shared_path.join(path).dirname }
|
|
38
|
+
dirs.each do |dir|
|
|
39
|
+
run_remote_ssh_command(test_dir_exists(dir))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
Then(/^the repo is cloned$/) do
|
|
44
|
+
run_remote_ssh_command(test_dir_exists(TestApp.repo_path))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
Then(/^the release is created$/) do
|
|
48
|
+
stdout, _stderr = run_remote_ssh_command("ls #{TestApp.releases_path}")
|
|
49
|
+
|
|
50
|
+
expect(stdout.strip).to match(/\A#{Time.now.utc.strftime("%Y%m%d")}\d{6}\Z/)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
Then(/^the REVISION file is created in the release$/) do
|
|
54
|
+
stdout, _stderr = run_remote_ssh_command("cat #{@release_paths[0]}/REVISION")
|
|
55
|
+
|
|
56
|
+
expect(stdout.strip).to match(/\h{40}/)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
Then(/^the REVISION_TIME file is created in the release$/) do
|
|
60
|
+
stdout, _stderr = run_remote_ssh_command("cat #{@release_paths[0]}/REVISION_TIME")
|
|
61
|
+
|
|
62
|
+
expect(stdout.strip).to match(/\d{10}/)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
Then(/^file symlinks are created in the new release$/) do
|
|
66
|
+
TestApp.linked_files.each do |file|
|
|
67
|
+
run_remote_ssh_command(test_symlink_exists(TestApp.current_path.join(file)))
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
Then(/^directory symlinks are created in the new release$/) do
|
|
72
|
+
pending
|
|
73
|
+
TestApp.linked_dirs.each do |dir|
|
|
74
|
+
run_remote_ssh_command(test_symlink_exists(TestApp.release_path.join(dir)))
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
Then(/^the current directory will be a symlink to the release$/) do
|
|
79
|
+
run_remote_ssh_command(exists?("e", TestApp.current_path))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
Then(/^the deploy\.rb file is created$/) do
|
|
83
|
+
file = TestApp.test_app_path.join("config/deploy.rb")
|
|
84
|
+
expect(File.exist?(file)).to be true
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
Then(/^the default stage files are created$/) do
|
|
88
|
+
staging = TestApp.test_app_path.join("config/deploy/staging.rb")
|
|
89
|
+
production = TestApp.test_app_path.join("config/deploy/production.rb")
|
|
90
|
+
expect(File.exist?(staging)).to be true
|
|
91
|
+
expect(File.exist?(production)).to be true
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
Then(/^the tasks folder is created$/) do
|
|
95
|
+
path = TestApp.test_app_path.join("lib/capistrano/tasks")
|
|
96
|
+
expect(Dir.exist?(path)).to be true
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
Then(/^the specified stage files are created$/) do
|
|
100
|
+
qa = TestApp.test_app_path.join("config/deploy/qa.rb")
|
|
101
|
+
production = TestApp.test_app_path.join("config/deploy/production.rb")
|
|
102
|
+
expect(File.exist?(qa)).to be true
|
|
103
|
+
expect(File.exist?(production)).to be true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
Then(/^it creates the file with the remote_task prerequisite$/) do
|
|
107
|
+
TestApp.linked_files.each do |file|
|
|
108
|
+
run_remote_ssh_command(test_file_exists(TestApp.shared_path.join(file)))
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
Then(/^it will not recreate the file$/) do
|
|
113
|
+
#
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
Then(/^the task is successful$/) do
|
|
117
|
+
expect(@success).to be true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
Then(/^the task fails$/) do
|
|
121
|
+
expect(@success).to be_falsey
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
Then(/^the failure task will run$/) do
|
|
125
|
+
failed = TestApp.shared_path.join("failed")
|
|
126
|
+
run_remote_ssh_command(test_file_exists(failed))
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
Then(/^the failure task will not run$/) do
|
|
130
|
+
failed = TestApp.shared_path.join("failed")
|
|
131
|
+
expect { run_remote_ssh_command(test_file_exists(failed)) }
|
|
132
|
+
.to raise_error(RemoteSSHHelpers::RemoteSSHCommandError)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
When(/^an error is raised$/) do
|
|
136
|
+
error = TestApp.shared_path.join("fail")
|
|
137
|
+
run_remote_ssh_command(test_file_exists(error))
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
Then(/contains "([^"]*)" in the output/) do |expected|
|
|
141
|
+
expect(@output).to include(expected)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
Then(/the output matches "([^"]*)" followed by "([^"]*)"/) do |expected, followedby|
|
|
145
|
+
expect(@output).to match(/#{expected}.*#{followedby}/m)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
Then(/doesn't contain "([^"]*)" in the output/) do |expected|
|
|
149
|
+
expect(@output).not_to include(expected)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
Then(/the current symlink points to the previous release/) do
|
|
153
|
+
previous_release_path = @release_paths[-2]
|
|
154
|
+
|
|
155
|
+
run_remote_ssh_command(symlinked?(TestApp.current_path, previous_release_path))
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
Then(/^the current symlink points to that specific release$/) do
|
|
159
|
+
specific_release_path = TestApp.releases_path.join(@rollback_release)
|
|
160
|
+
|
|
161
|
+
run_remote_ssh_command(symlinked?(TestApp.current_path, specific_release_path))
|
|
162
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
When(/^I run cap "(.*?)"$/) do |task|
|
|
2
|
+
@success, @output = TestApp.cap(task)
|
|
3
|
+
end
|
|
4
|
+
|
|
5
|
+
When(/^I run cap "(.*?)" within the "(.*?)" directory$/) do |task, directory|
|
|
6
|
+
@success, @output = TestApp.cap(task, directory)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
When(/^I run cap "(.*?)" as part of a release$/) do |task|
|
|
10
|
+
TestApp.cap("deploy:new_release_path #{task}")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
When(/^I run "(.*?)"$/) do |command|
|
|
14
|
+
@success, @output = TestApp.run(command)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
When(/^I rollback to a specific release$/) do
|
|
18
|
+
@rollback_release = @release_paths.first.split("/").last
|
|
19
|
+
|
|
20
|
+
step %Q{I run cap "deploy:rollback ROLLBACK_RELEASE=#{@rollback_release}"}
|
|
21
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Feature: cap can be run from a subdirectory, and will still find the Capfile
|
|
2
|
+
|
|
3
|
+
Background:
|
|
4
|
+
Given a test app with the default configuration
|
|
5
|
+
And servers with the roles app and web
|
|
6
|
+
|
|
7
|
+
Scenario: Running cap from a subdirectory
|
|
8
|
+
When I run cap "git:check" within the "config" directory
|
|
9
|
+
Then the task is successful
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require_relative "../../spec/support/test_app"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module RemoteCommandHelpers
|
|
2
|
+
def test_dir_exists(path)
|
|
3
|
+
exists?("d", path)
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def test_symlink_exists(path)
|
|
7
|
+
exists?("L", path)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_file_exists(path)
|
|
11
|
+
exists?("f", path)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def exists?(type, path)
|
|
15
|
+
%Q{[[ -#{type} "#{path}" ]]}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def symlinked?(symlink_path, target_path)
|
|
19
|
+
"[ #{symlink_path} -ef #{target_path} ]"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def safely_remove_file(_path)
|
|
23
|
+
run_remote_ssh_command("rm #{test_file}")
|
|
24
|
+
rescue
|
|
25
|
+
RemoteSSHHelpers::RemoteSSHCommandError
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
World(RemoteCommandHelpers)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
require "socket"
|
|
3
|
+
require_relative "docker_gateway"
|
|
4
|
+
|
|
5
|
+
module RemoteSSHHelpers
|
|
6
|
+
extend self
|
|
7
|
+
|
|
8
|
+
class RemoteSSHCommandError < RuntimeError; end
|
|
9
|
+
|
|
10
|
+
def start_ssh_server
|
|
11
|
+
docker_gateway.start
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def wait_for_ssh_server(retries=3)
|
|
15
|
+
Socket.tcp("localhost", 2022, connect_timeout: 1).close
|
|
16
|
+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT
|
|
17
|
+
retries -= 1
|
|
18
|
+
sleep(2) && retry if retries.positive?
|
|
19
|
+
raise
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def run_remote_ssh_command(command)
|
|
23
|
+
stdout, stderr, status = docker_gateway.run_shell_command(command)
|
|
24
|
+
return [stdout, stderr] if status.success?
|
|
25
|
+
raise RemoteSSHCommandError, status
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def docker_gateway
|
|
29
|
+
@docker_gateway ||= DockerGateway.new(method(:log))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
World(RemoteSSHHelpers)
|
data/lib/Capfile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require "rake"
|
|
2
|
+
require "sshkit"
|
|
3
|
+
|
|
4
|
+
require "io/console"
|
|
5
|
+
|
|
6
|
+
Rake.application.options.trace = true
|
|
7
|
+
|
|
8
|
+
require "capistrano/version"
|
|
9
|
+
require "capistrano/version_validator"
|
|
10
|
+
require "capistrano/i18n"
|
|
11
|
+
require "capistrano/dsl"
|
|
12
|
+
require "capistrano/application"
|
|
13
|
+
require "capistrano/configuration"
|
|
14
|
+
require "capistrano/configuration/scm_resolver"
|
|
15
|
+
|
|
16
|
+
module Capistrano
|
|
17
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
module Capistrano
|
|
2
|
+
class Application < Rake::Application
|
|
3
|
+
def initialize
|
|
4
|
+
super
|
|
5
|
+
@rakefiles = %w{capfile Capfile capfile.rb Capfile.rb}
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def name
|
|
9
|
+
"cap"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run
|
|
13
|
+
Rake.application = self
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def sort_options(options)
|
|
18
|
+
not_applicable_to_capistrano = %w(quiet silent verbose)
|
|
19
|
+
options.reject! do |(switch, *)|
|
|
20
|
+
switch =~ /--#{Regexp.union(not_applicable_to_capistrano)}/
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
super.push(version, dry_run, roles, hostfilter, print_config_variables)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def handle_options
|
|
27
|
+
options.rakelib = ["rakelib"]
|
|
28
|
+
options.trace_output = $stderr
|
|
29
|
+
|
|
30
|
+
OptionParser.new do |opts|
|
|
31
|
+
opts.banner = "See full documentation at http://capistranorb.com/."
|
|
32
|
+
opts.separator ""
|
|
33
|
+
opts.separator "Install capistrano in a project:"
|
|
34
|
+
opts.separator " bundle exec cap install [STAGES=qa,staging,production,...]"
|
|
35
|
+
opts.separator ""
|
|
36
|
+
opts.separator "Show available tasks:"
|
|
37
|
+
opts.separator " bundle exec cap -T"
|
|
38
|
+
opts.separator ""
|
|
39
|
+
opts.separator "Invoke (or simulate invoking) a task:"
|
|
40
|
+
opts.separator " bundle exec cap [--dry-run] STAGE TASK"
|
|
41
|
+
opts.separator ""
|
|
42
|
+
opts.separator "Advanced options:"
|
|
43
|
+
|
|
44
|
+
opts.on_tail("-h", "--help", "-H", "Display this help message.") do
|
|
45
|
+
puts opts
|
|
46
|
+
exit
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
standard_rake_options.each { |args| opts.on(*args) }
|
|
50
|
+
opts.environment("RAKEOPT")
|
|
51
|
+
end.parse!
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def top_level_tasks
|
|
55
|
+
if tasks_without_stage_dependency.include?(@top_level_tasks.first)
|
|
56
|
+
@top_level_tasks
|
|
57
|
+
else
|
|
58
|
+
@top_level_tasks.unshift(ensure_stage.to_s)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def display_error_message(ex)
|
|
63
|
+
unless options.backtrace
|
|
64
|
+
Rake.application.options.suppress_backtrace_pattern = backtrace_pattern if backtrace_pattern
|
|
65
|
+
trace "(Backtrace restricted to imported tasks)"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
super
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def exit_because_of_exception(ex)
|
|
72
|
+
if respond_to?(:deploying?) && deploying?
|
|
73
|
+
exit_deploy_because_of_exception(ex)
|
|
74
|
+
else
|
|
75
|
+
super
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# allows the `cap install` task to load without a capfile
|
|
80
|
+
def find_rakefile_location
|
|
81
|
+
if (location = super).nil?
|
|
82
|
+
[capfile, Dir.pwd]
|
|
83
|
+
else
|
|
84
|
+
location
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def backtrace_pattern
|
|
91
|
+
loc = Rake.application.find_rakefile_location
|
|
92
|
+
return unless loc
|
|
93
|
+
|
|
94
|
+
whitelist = (@imported.dup << loc[0]).map { |f| File.absolute_path(f, loc[1]) }
|
|
95
|
+
/^(?!#{whitelist.map { |p| Regexp.quote(p) }.join('|')})/
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def load_imports
|
|
99
|
+
if options.show_tasks && Rake::Task.task_defined?("load:defaults")
|
|
100
|
+
invoke "load:defaults"
|
|
101
|
+
set(:stage, "")
|
|
102
|
+
Dir[deploy_config_path].each { |f| add_import f }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
super
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def capfile
|
|
109
|
+
File.expand_path(File.join(File.dirname(__FILE__), "..", "Capfile"))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def version
|
|
113
|
+
["--version", "-V",
|
|
114
|
+
"Display the program version.",
|
|
115
|
+
lambda do |_value|
|
|
116
|
+
puts "Capistrano Version: #{Capistrano::VERSION} (Rake Version: #{Rake::VERSION})"
|
|
117
|
+
exit
|
|
118
|
+
end]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def dry_run
|
|
122
|
+
["--dry-run", "-n",
|
|
123
|
+
"Do a dry run without executing actions",
|
|
124
|
+
lambda do |_value|
|
|
125
|
+
Configuration.env.set(:sshkit_backend, SSHKit::Backend::Printer)
|
|
126
|
+
end]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def roles
|
|
130
|
+
["--roles ROLES", "-r",
|
|
131
|
+
"Run SSH commands only on hosts matching these roles",
|
|
132
|
+
lambda do |value|
|
|
133
|
+
Configuration.env.add_cmdline_filter(:role, value)
|
|
134
|
+
end]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def hostfilter
|
|
138
|
+
["--hosts HOSTS", "-z",
|
|
139
|
+
"Run SSH commands only on matching hosts",
|
|
140
|
+
lambda do |value|
|
|
141
|
+
Configuration.env.add_cmdline_filter(:host, value)
|
|
142
|
+
end]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def print_config_variables
|
|
146
|
+
["--print-config-variables", "-p",
|
|
147
|
+
"Display the defined config variables before starting the deployment tasks.",
|
|
148
|
+
lambda do |_value|
|
|
149
|
+
Configuration.env.set(:print_config_variables, true)
|
|
150
|
+
end]
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|