capistrano 3.11.0 → 3.19.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- 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/pull_request_template.md +0 -4
- data/.github/release-drafter.yml +25 -0
- data/.github/workflows/ci.yml +80 -0
- data/.github/workflows/release-drafter.yml +18 -0
- data/.rubocop.yml +4 -3
- data/CHANGELOG.md +1 -651
- data/DEVELOPMENT.md +5 -20
- data/Gemfile +40 -3
- data/LICENSE.txt +1 -1
- data/README.md +3 -3
- data/RELEASING.md +3 -3
- data/Rakefile +13 -5
- data/capistrano.gemspec +8 -7
- data/docker-compose.yml +8 -0
- data/features/deploy.feature +11 -1
- data/features/sshconnect.feature +1 -1
- data/features/step_definitions/assertions.rb +34 -24
- data/features/step_definitions/setup.rb +15 -16
- data/features/support/docker_gateway.rb +53 -0
- data/features/support/env.rb +0 -10
- data/features/support/remote_command_helpers.rb +3 -3
- data/features/support/remote_ssh_helpers.rb +33 -0
- data/lib/capistrano/configuration/question.rb +16 -4
- data/lib/capistrano/configuration/validated_variables.rb +1 -1
- data/lib/capistrano/doctor/variables_doctor.rb +2 -0
- data/lib/capistrano/dsl.rb +1 -1
- data/lib/capistrano/i18n.rb +2 -0
- data/lib/capistrano/scm/git.rb +15 -4
- data/lib/capistrano/scm/tasks/git.rake +19 -7
- data/lib/capistrano/tasks/deploy.rake +26 -3
- data/lib/capistrano/templates/deploy.rb.erb +2 -2
- data/lib/capistrano/templates/stage.rb.erb +1 -1
- data/lib/capistrano/version.rb +1 -1
- data/spec/integration/dsl_spec.rb +16 -14
- data/spec/lib/capistrano/application_spec.rb +16 -40
- data/spec/lib/capistrano/configuration/plugin_installer_spec.rb +1 -1
- data/spec/lib/capistrano/configuration/question_spec.rb +31 -13
- data/spec/lib/capistrano/configuration/scm_resolver_spec.rb +4 -2
- data/spec/lib/capistrano/doctor/environment_doctor_spec.rb +1 -1
- data/spec/lib/capistrano/doctor/gems_doctor_spec.rb +1 -1
- data/spec/lib/capistrano/doctor/servers_doctor_spec.rb +1 -1
- data/spec/lib/capistrano/doctor/variables_doctor_spec.rb +1 -1
- data/spec/lib/capistrano/dsl/task_enhancements_spec.rb +6 -6
- data/spec/lib/capistrano/dsl_spec.rb +5 -5
- data/spec/lib/capistrano/plugin_spec.rb +2 -2
- data/spec/lib/capistrano/scm/git_spec.rb +37 -5
- data/spec/spec_helper.rb +13 -0
- data/spec/support/test_app.rb +23 -14
- metadata +25 -73
- data/.travis.yml +0 -27
- data/Dangerfile +0 -1
- data/features/support/vagrant_helpers.rb +0 -35
- data/spec/support/.gitignore +0 -1
- data/spec/support/Vagrantfile +0 -23
data/DEVELOPMENT.md
CHANGED
@@ -20,22 +20,16 @@ Everyone can help improve Capistrano. There are ways to contribute even if you a
|
|
20
20
|
|
21
21
|
## Contributing documentation
|
22
22
|
|
23
|
-
Improvements and additions to Capistrano's documentation are very much appreciated. The official
|
23
|
+
Improvements and additions to Capistrano's documentation are very much appreciated. The official documentation is stored in the `docs/` directory as Markdown files. These files are used to automatically generate the [capistranorb.com](https://capistranorb.com/) website, which is hosted by GitHub Pages. Feel free to make changes to this documentation as you see fit. Before opening a pull request, make sure your documentation renders correctly by previewing the website in your local environment. Refer to [docs/README.md][] for instructions.
|
24
24
|
|
25
25
|
## Setting up your development environment
|
26
26
|
|
27
|
-
Capistrano is a Ruby project, so we expect you to have a functioning Ruby environment
|
28
|
-
|
29
|
-
Make sure to install:
|
30
|
-
|
31
|
-
* [Bundler](https://bundler.io/)
|
32
|
-
* [Vagrant](https://www.vagrantup.com/)
|
33
|
-
* [VirtualBox](https://www.virtualbox.org/wiki/Downloads) (or another [Vagrant-supported](https://docs.vagrantup.com/v2/getting-started/providers.html) VM host)
|
27
|
+
Capistrano is a Ruby project, so we expect you to have a functioning Ruby environment with the latest stable version of Ruby. To run Cucumber tests, you'll also need [Docker installed](https://docs.docker.com/get-docker/) and running.
|
34
28
|
|
35
29
|
|
36
30
|
### Running tests
|
37
31
|
|
38
|
-
Capistrano has two test suites: an RSpec suite and a Cucumber suite. The RSpec suite handles quick feedback unit specs. The Cucumber suite is an integration suite that uses
|
32
|
+
Capistrano has two test suites: an RSpec suite and a Cucumber suite. The RSpec suite handles quick feedback unit specs. The Cucumber suite is an integration suite that uses a Docker container as an SSH server and deployment target.
|
39
33
|
|
40
34
|
```
|
41
35
|
# Ensure all dependencies are installed
|
@@ -44,26 +38,17 @@ $ bundle install
|
|
44
38
|
# Run the RSpec suite
|
45
39
|
$ bundle exec rake spec
|
46
40
|
|
47
|
-
# Run the Cucumber suite
|
41
|
+
# Run the Cucumber suite (Docker must be running)
|
48
42
|
$ bundle exec rake features
|
49
|
-
|
50
|
-
# Run the Cucumber suite and leave the VM running (faster for subsequent runs)
|
51
|
-
$ bundle exec rake features KEEP_RUNNING=1
|
52
43
|
```
|
53
44
|
|
54
|
-
### Report failing Cucumber features!
|
55
|
-
|
56
|
-
Currently, the Capistrano Travis build does *not* run the Cucumber suite. This means it is possible for a failing Cucumber feature to sneak in without being noticed by our continuous integration checks.
|
57
|
-
|
58
|
-
**If you come across a failing Cucumber feature, this is a bug.** Please report it by opening a GitHub issue. Or even better: do your best to fix the feature and submit a pull request!
|
59
|
-
|
60
45
|
## Coding guidelines
|
61
46
|
|
62
47
|
This project uses [RuboCop](https://github.com/bbatsov/rubocop) to enforce standard Ruby coding guidelines.
|
63
48
|
|
64
49
|
* Test that your contributions pass with `rake rubocop`
|
65
50
|
* Rubocop is also run as part of the full test suite with `rake`
|
66
|
-
* Note the
|
51
|
+
* Note the CI build will fail and your PR cannot be merged if Rubocop finds errors
|
67
52
|
|
68
53
|
## Submitting a pull request
|
69
54
|
|
data/Gemfile
CHANGED
@@ -3,8 +3,45 @@ source "https://rubygems.org"
|
|
3
3
|
# Specify your gem's dependencies in capistrano.gemspec
|
4
4
|
gemspec
|
5
5
|
|
6
|
+
gem "mocha"
|
7
|
+
gem "rspec"
|
8
|
+
gem "rspec-core", "~> 3.4.4"
|
9
|
+
|
6
10
|
group :cucumber do
|
7
|
-
|
8
|
-
|
9
|
-
|
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"
|
10
47
|
end
|
data/LICENSE.txt
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
MIT License (MIT)
|
2
2
|
|
3
|
-
Copyright (c) 2012-
|
3
|
+
Copyright (c) 2012-2020 Tom Clements, Lee Hambley
|
4
4
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
|
2
2
|
# Capistrano: A deployment automation tool built on Ruby, Rake, and SSH.
|
3
3
|
|
4
|
-
[![Gem Version](https://badge.fury.io/rb/capistrano.svg)](http://badge.fury.io/rb/capistrano) [![Build Status](https://
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/capistrano.svg)](http://badge.fury.io/rb/capistrano) [![Build Status](https://github.com/capistrano/capistrano/actions/workflows/ci.yml/badge.svg)](https://github.com/capistrano/capistrano/actions/workflows/ci.yml) [![Code Climate](https://codeclimate.com/github/capistrano/capistrano/badges/gpa.svg)](https://codeclimate.com/github/capistrano/capistrano) [![CodersClan](https://img.shields.io/badge/get-support-blue.svg)](http://codersclan.net/?repo_id=325&source=small)
|
5
5
|
|
6
6
|
Capistrano is a framework for building automated deployment scripts. Although Capistrano itself is written in Ruby, it can easily be used to deploy projects of any language or framework, be it Rails, Java, or PHP.
|
7
7
|
|
@@ -107,7 +107,7 @@ Add Capistrano to your project's Gemfile using `require: false`:
|
|
107
107
|
|
108
108
|
``` ruby
|
109
109
|
group :development do
|
110
|
-
gem "capistrano", "~> 3.
|
110
|
+
gem "capistrano", "~> 3.17", require: false
|
111
111
|
end
|
112
112
|
```
|
113
113
|
|
@@ -200,7 +200,7 @@ Contributions to Capistrano, in the form of code, documentation or idea, are gla
|
|
200
200
|
|
201
201
|
MIT License (MIT)
|
202
202
|
|
203
|
-
Copyright (c) 2012-
|
203
|
+
Copyright (c) 2012-2020 Tom Clements, Lee Hambley
|
204
204
|
|
205
205
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
206
206
|
of this software and associated documentation files (the "Software"), to deal
|
data/RELEASING.md
CHANGED
@@ -11,7 +11,7 @@
|
|
11
11
|
2. **Ensure all tests are passing by running `rake spec` and `rake features`.**
|
12
12
|
3. Determine which would be the correct next version number according to [semver](http://semver.org/).
|
13
13
|
4. Update the version in `./lib/capistrano/version.rb`.
|
14
|
-
|
15
|
-
|
16
|
-
6. Commit the changelog and version in a single commit, the message should be "Preparing vX.Y.Z"
|
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"
|
17
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
|
data/Rakefile
CHANGED
@@ -1,12 +1,20 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
require "cucumber/rake/task"
|
3
3
|
require "rspec/core/rake_task"
|
4
|
-
require "rubocop/rake_task"
|
5
4
|
|
6
|
-
|
7
|
-
|
5
|
+
begin
|
6
|
+
require "rubocop/rake_task"
|
7
|
+
desc "Run RuboCop checks"
|
8
|
+
RuboCop::RakeTask.new
|
9
|
+
task default: %i(spec rubocop)
|
10
|
+
rescue LoadError
|
11
|
+
task default: :spec
|
12
|
+
end
|
8
13
|
|
14
|
+
RSpec::Core::RakeTask.new
|
9
15
|
Cucumber::Rake::Task.new(:features)
|
10
16
|
|
11
|
-
|
12
|
-
|
17
|
+
Rake::Task["release"].enhance do
|
18
|
+
puts "Don't forget to publish the release on GitHub!"
|
19
|
+
system "open https://github.com/capistrano/capistrano/releases"
|
20
|
+
end
|
data/capistrano.gemspec
CHANGED
@@ -11,8 +11,14 @@ Gem::Specification.new do |gem|
|
|
11
11
|
gem.email = ["seenmyfate@gmail.com", "lee.hambley@gmail.com"]
|
12
12
|
gem.description = "Capistrano is a utility and framework for executing commands in parallel on multiple remote machines, via SSH."
|
13
13
|
gem.summary = "Capistrano - Welcome to easy deployment with Ruby over SSH"
|
14
|
-
gem.homepage = "
|
15
|
-
|
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
|
+
}
|
16
22
|
gem.files = `git ls-files -z`.split("\x0").reject { |f| f =~ /^docs/ }
|
17
23
|
gem.executables = %w(cap capify)
|
18
24
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
@@ -25,9 +31,4 @@ Gem::Specification.new do |gem|
|
|
25
31
|
gem.add_dependency "i18n"
|
26
32
|
gem.add_dependency "rake", ">= 10.0.0"
|
27
33
|
gem.add_dependency "sshkit", ">= 1.9.0"
|
28
|
-
|
29
|
-
gem.add_development_dependency "danger"
|
30
|
-
gem.add_development_dependency "mocha"
|
31
|
-
gem.add_development_dependency "rspec"
|
32
|
-
gem.add_development_dependency "rubocop", "0.48.1"
|
33
34
|
end
|
data/docker-compose.yml
ADDED
data/features/deploy.feature
CHANGED
@@ -40,6 +40,11 @@ Feature: Deploy
|
|
40
40
|
Then the repo is cloned
|
41
41
|
And the release is created
|
42
42
|
|
43
|
+
Scenario: REVISION and REVISION_TIME files are present
|
44
|
+
When I make 1 deployment
|
45
|
+
Then the REVISION file is created in the release
|
46
|
+
Then the REVISION_TIME file is created in the release
|
47
|
+
|
43
48
|
Scenario: Symlink linked files
|
44
49
|
When I run cap "deploy:symlink:linked_files deploy:symlink:release" as part of a release
|
45
50
|
Then file symlinks are created in the new release
|
@@ -70,6 +75,12 @@ Feature: Deploy
|
|
70
75
|
Then 3 valid releases are kept
|
71
76
|
And the current directory will be a symlink to the release
|
72
77
|
|
78
|
+
Scenario: Cleanup when there are more releases than arguments can handle
|
79
|
+
Given config stage file has line "set :keep_releases, 3"
|
80
|
+
And 5000 valid existing releases
|
81
|
+
When I run cap "deploy:cleanup"
|
82
|
+
Then 3 valid releases are kept
|
83
|
+
|
73
84
|
Scenario: Rolling Back
|
74
85
|
Given I make 2 deployments
|
75
86
|
When I run cap "deploy:rollback"
|
@@ -79,4 +90,3 @@ Feature: Deploy
|
|
79
90
|
Given I make 3 deployments
|
80
91
|
When I rollback to a specific release
|
81
92
|
Then the current symlink points to that specific release
|
82
|
-
|
data/features/sshconnect.feature
CHANGED
@@ -8,4 +8,4 @@ Feature: SSH Connection
|
|
8
8
|
Scenario: Switching from default user to root and back again
|
9
9
|
When I run cap "am_i_root"
|
10
10
|
Then the task is successful
|
11
|
-
And the output matches "I am uid=0\(root\)" followed by "I am uid=\d+\(
|
11
|
+
And the output matches "I am uid=0\(root\)" followed by "I am uid=\d+\(deployer\)"
|
@@ -5,68 +5,78 @@ Then(/^references in the remote repo are listed$/) do
|
|
5
5
|
end
|
6
6
|
|
7
7
|
Then(/^git wrapper permissions are 0700$/) do
|
8
|
-
permissions_test = %Q([ $(stat -c "%a" #{TestApp.
|
9
|
-
|
10
|
-
|
11
|
-
expect(status).to be_success
|
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
|
12
10
|
end
|
13
11
|
|
14
12
|
Then(/^the shared path is created$/) do
|
15
|
-
|
13
|
+
run_remote_ssh_command(test_dir_exists(TestApp.shared_path))
|
16
14
|
end
|
17
15
|
|
18
16
|
Then(/^the releases path is created$/) do
|
19
|
-
|
17
|
+
run_remote_ssh_command(test_dir_exists(TestApp.releases_path))
|
20
18
|
end
|
21
19
|
|
22
20
|
Then(/^(\d+) valid releases are kept/) do |num|
|
23
21
|
test = %Q([ $(ls -g #{TestApp.releases_path} | grep -E '[0-9]{14}' | wc -l) == "#{num}" ])
|
24
|
-
|
25
|
-
expect(status).to be_success
|
22
|
+
expect { run_remote_ssh_command(test) }.not_to raise_error
|
26
23
|
end
|
27
24
|
|
28
25
|
Then(/^the invalid (.+) release is ignored$/) do |filename|
|
29
26
|
test = "ls -g #{TestApp.releases_path} | grep #{filename}"
|
30
|
-
|
31
|
-
expect(status).to be_success
|
27
|
+
expect { run_remote_ssh_command(test) }.not_to raise_error
|
32
28
|
end
|
33
29
|
|
34
30
|
Then(/^directories in :linked_dirs are created in shared$/) do
|
35
31
|
TestApp.linked_dirs.each do |dir|
|
36
|
-
|
32
|
+
run_remote_ssh_command(test_dir_exists(TestApp.shared_path.join(dir)))
|
37
33
|
end
|
38
34
|
end
|
39
35
|
|
40
36
|
Then(/^directories referenced in :linked_files are created in shared$/) do
|
41
37
|
dirs = TestApp.linked_files.map { |path| TestApp.shared_path.join(path).dirname }
|
42
38
|
dirs.each do |dir|
|
43
|
-
|
39
|
+
run_remote_ssh_command(test_dir_exists(dir))
|
44
40
|
end
|
45
41
|
end
|
46
42
|
|
47
43
|
Then(/^the repo is cloned$/) do
|
48
|
-
|
44
|
+
run_remote_ssh_command(test_dir_exists(TestApp.repo_path))
|
49
45
|
end
|
50
46
|
|
51
47
|
Then(/^the release is created$/) do
|
52
|
-
|
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}/)
|
53
63
|
end
|
54
64
|
|
55
65
|
Then(/^file symlinks are created in the new release$/) do
|
56
66
|
TestApp.linked_files.each do |file|
|
57
|
-
|
67
|
+
run_remote_ssh_command(test_symlink_exists(TestApp.current_path.join(file)))
|
58
68
|
end
|
59
69
|
end
|
60
70
|
|
61
71
|
Then(/^directory symlinks are created in the new release$/) do
|
62
72
|
pending
|
63
73
|
TestApp.linked_dirs.each do |dir|
|
64
|
-
|
74
|
+
run_remote_ssh_command(test_symlink_exists(TestApp.release_path.join(dir)))
|
65
75
|
end
|
66
76
|
end
|
67
77
|
|
68
78
|
Then(/^the current directory will be a symlink to the release$/) do
|
69
|
-
|
79
|
+
run_remote_ssh_command(exists?("e", TestApp.current_path))
|
70
80
|
end
|
71
81
|
|
72
82
|
Then(/^the deploy\.rb file is created$/) do
|
@@ -95,7 +105,7 @@ end
|
|
95
105
|
|
96
106
|
Then(/^it creates the file with the remote_task prerequisite$/) do
|
97
107
|
TestApp.linked_files.each do |file|
|
98
|
-
|
108
|
+
run_remote_ssh_command(test_file_exists(TestApp.shared_path.join(file)))
|
99
109
|
end
|
100
110
|
end
|
101
111
|
|
@@ -113,18 +123,18 @@ end
|
|
113
123
|
|
114
124
|
Then(/^the failure task will run$/) do
|
115
125
|
failed = TestApp.shared_path.join("failed")
|
116
|
-
|
126
|
+
run_remote_ssh_command(test_file_exists(failed))
|
117
127
|
end
|
118
128
|
|
119
129
|
Then(/^the failure task will not run$/) do
|
120
130
|
failed = TestApp.shared_path.join("failed")
|
121
|
-
expect {
|
122
|
-
.to raise_error(
|
131
|
+
expect { run_remote_ssh_command(test_file_exists(failed)) }
|
132
|
+
.to raise_error(RemoteSSHHelpers::RemoteSSHCommandError)
|
123
133
|
end
|
124
134
|
|
125
135
|
When(/^an error is raised$/) do
|
126
136
|
error = TestApp.shared_path.join("fail")
|
127
|
-
|
137
|
+
run_remote_ssh_command(test_file_exists(error))
|
128
138
|
end
|
129
139
|
|
130
140
|
Then(/contains "([^"]*)" in the output/) do |expected|
|
@@ -142,11 +152,11 @@ end
|
|
142
152
|
Then(/the current symlink points to the previous release/) do
|
143
153
|
previous_release_path = @release_paths[-2]
|
144
154
|
|
145
|
-
|
155
|
+
run_remote_ssh_command(symlinked?(TestApp.current_path, previous_release_path))
|
146
156
|
end
|
147
157
|
|
148
158
|
Then(/^the current symlink points to that specific release$/) do
|
149
159
|
specific_release_path = TestApp.releases_path.join(@rollback_release)
|
150
160
|
|
151
|
-
|
161
|
+
run_remote_ssh_command(symlinked?(TestApp.current_path, specific_release_path))
|
152
162
|
end
|
@@ -7,11 +7,8 @@ Given(/^a test app without any configuration$/) do
|
|
7
7
|
end
|
8
8
|
|
9
9
|
Given(/^servers with the roles app and web$/) do
|
10
|
-
|
11
|
-
|
12
|
-
rescue
|
13
|
-
nil
|
14
|
-
end
|
10
|
+
start_ssh_server
|
11
|
+
wait_for_ssh_server
|
15
12
|
end
|
16
13
|
|
17
14
|
Given(/^a linked file "(.*?)"$/) do |file|
|
@@ -21,8 +18,8 @@ end
|
|
21
18
|
|
22
19
|
Given(/^file "(.*?)" exists in shared path$/) do |file|
|
23
20
|
file_shared_path = TestApp.shared_path.join(file)
|
24
|
-
|
25
|
-
|
21
|
+
run_remote_ssh_command("mkdir -p #{file_shared_path.dirname}")
|
22
|
+
run_remote_ssh_command("touch #{file_shared_path}")
|
26
23
|
end
|
27
24
|
|
28
25
|
Given(/^all linked files exists in shared path$/) do
|
@@ -33,8 +30,8 @@ end
|
|
33
30
|
|
34
31
|
Given(/^file "(.*?)" does not exist in shared path$/) do |file|
|
35
32
|
file_shared_path = TestApp.shared_path.join(file)
|
36
|
-
|
37
|
-
|
33
|
+
run_remote_ssh_command("mkdir -p #{TestApp.shared_path}")
|
34
|
+
run_remote_ssh_command("touch #{file_shared_path} && rm #{file_shared_path}")
|
38
35
|
end
|
39
36
|
|
40
37
|
Given(/^a custom task to generate a file$/) do
|
@@ -67,12 +64,12 @@ Given(/^a stage file named (.+)$/) do |filename|
|
|
67
64
|
TestApp.write_local_stage_file(filename)
|
68
65
|
end
|
69
66
|
|
70
|
-
Given(/^I make (\d+) deployments
|
67
|
+
Given(/^I make (\d+) deployments?$/) do |count|
|
71
68
|
step "all linked files exists in shared path"
|
72
69
|
|
73
70
|
@release_paths = (1..count.to_i).map do
|
74
71
|
TestApp.cap("deploy")
|
75
|
-
stdout, _stderr =
|
72
|
+
stdout, _stderr = run_remote_ssh_command("readlink #{TestApp.current_path}")
|
76
73
|
|
77
74
|
stdout.strip
|
78
75
|
end
|
@@ -80,13 +77,15 @@ end
|
|
80
77
|
|
81
78
|
Given(/^(\d+) valid existing releases$/) do |num|
|
82
79
|
a_day = 86_400 # in seconds
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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(' ')}")
|
87
86
|
end
|
88
87
|
end
|
89
88
|
|
90
89
|
Given(/^an invalid release named "(.+)"$/) do |filename|
|
91
|
-
|
90
|
+
run_remote_ssh_command("mkdir -p #{TestApp.release_path(filename)}")
|
92
91
|
end
|
@@ -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
|
data/features/support/env.rb
CHANGED
@@ -1,11 +1 @@
|
|
1
|
-
PROJECT_ROOT = File.expand_path("../../../", __FILE__)
|
2
|
-
VAGRANT_ROOT = File.join(PROJECT_ROOT, "spec/support")
|
3
|
-
VAGRANT_BIN = ENV["VAGRANT_BIN"] || "vagrant"
|
4
|
-
|
5
|
-
at_exit do
|
6
|
-
if ENV["KEEP_RUNNING"]
|
7
|
-
VagrantHelpers.run_vagrant_command("rm -rf /home/vagrant/var")
|
8
|
-
end
|
9
|
-
end
|
10
|
-
|
11
1
|
require_relative "../../spec/support/test_app"
|
@@ -12,7 +12,7 @@ module RemoteCommandHelpers
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def exists?(type, path)
|
15
|
-
%Q{[ -#{type} "#{path}" ]}
|
15
|
+
%Q{[[ -#{type} "#{path}" ]]}
|
16
16
|
end
|
17
17
|
|
18
18
|
def symlinked?(symlink_path, target_path)
|
@@ -20,9 +20,9 @@ module RemoteCommandHelpers
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def safely_remove_file(_path)
|
23
|
-
|
23
|
+
run_remote_ssh_command("rm #{test_file}")
|
24
24
|
rescue
|
25
|
-
|
25
|
+
RemoteSSHHelpers::RemoteSSHCommandError
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
@@ -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)
|
@@ -36,12 +36,12 @@ module Capistrano
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def gets
|
39
|
-
return unless
|
39
|
+
return unless stdin.tty?
|
40
40
|
|
41
41
|
if echo?
|
42
|
-
|
42
|
+
stdin.gets
|
43
43
|
else
|
44
|
-
|
44
|
+
stdin.noecho(&:gets).tap { $stdout.print "\n" }
|
45
45
|
end
|
46
46
|
rescue Errno::EIO
|
47
47
|
# when stdio gets closed
|
@@ -49,7 +49,11 @@ module Capistrano
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def question
|
52
|
-
if default.nil?
|
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?
|
53
57
|
I18n.t(:question, key: key, scope: :capistrano)
|
54
58
|
else
|
55
59
|
I18n.t(:question_default, key: key, default_value: default, scope: :capistrano)
|
@@ -59,6 +63,14 @@ module Capistrano
|
|
59
63
|
def echo?
|
60
64
|
(options || {}).fetch(:echo, true)
|
61
65
|
end
|
66
|
+
|
67
|
+
def stdin
|
68
|
+
(options || {}).fetch(:stdin, $stdin)
|
69
|
+
end
|
70
|
+
|
71
|
+
def prompt
|
72
|
+
(options || {}).fetch(:prompt, nil)
|
73
|
+
end
|
62
74
|
end
|
63
75
|
end
|
64
76
|
end
|