capistrano 3.11.0 → 3.19.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.docker/Dockerfile +7 -0
  3. data/.docker/ssh_key_rsa +49 -0
  4. data/.docker/ssh_key_rsa.pub +1 -0
  5. data/.docker/ubuntu_setup.sh +23 -0
  6. data/.github/pull_request_template.md +0 -4
  7. data/.github/release-drafter.yml +25 -0
  8. data/.github/workflows/ci.yml +80 -0
  9. data/.github/workflows/release-drafter.yml +18 -0
  10. data/.rubocop.yml +4 -3
  11. data/CHANGELOG.md +1 -651
  12. data/DEVELOPMENT.md +5 -20
  13. data/Gemfile +40 -3
  14. data/LICENSE.txt +1 -1
  15. data/README.md +3 -3
  16. data/RELEASING.md +3 -3
  17. data/Rakefile +13 -5
  18. data/capistrano.gemspec +8 -7
  19. data/docker-compose.yml +8 -0
  20. data/features/deploy.feature +11 -1
  21. data/features/sshconnect.feature +1 -1
  22. data/features/step_definitions/assertions.rb +34 -24
  23. data/features/step_definitions/setup.rb +15 -16
  24. data/features/support/docker_gateway.rb +53 -0
  25. data/features/support/env.rb +0 -10
  26. data/features/support/remote_command_helpers.rb +3 -3
  27. data/features/support/remote_ssh_helpers.rb +33 -0
  28. data/lib/capistrano/configuration/question.rb +16 -4
  29. data/lib/capistrano/configuration/validated_variables.rb +1 -1
  30. data/lib/capistrano/doctor/variables_doctor.rb +2 -0
  31. data/lib/capistrano/dsl.rb +1 -1
  32. data/lib/capistrano/i18n.rb +2 -0
  33. data/lib/capistrano/scm/git.rb +15 -4
  34. data/lib/capistrano/scm/tasks/git.rake +19 -7
  35. data/lib/capistrano/tasks/deploy.rake +26 -3
  36. data/lib/capistrano/templates/deploy.rb.erb +2 -2
  37. data/lib/capistrano/templates/stage.rb.erb +1 -1
  38. data/lib/capistrano/version.rb +1 -1
  39. data/spec/integration/dsl_spec.rb +16 -14
  40. data/spec/lib/capistrano/application_spec.rb +16 -40
  41. data/spec/lib/capistrano/configuration/plugin_installer_spec.rb +1 -1
  42. data/spec/lib/capistrano/configuration/question_spec.rb +31 -13
  43. data/spec/lib/capistrano/configuration/scm_resolver_spec.rb +4 -2
  44. data/spec/lib/capistrano/doctor/environment_doctor_spec.rb +1 -1
  45. data/spec/lib/capistrano/doctor/gems_doctor_spec.rb +1 -1
  46. data/spec/lib/capistrano/doctor/servers_doctor_spec.rb +1 -1
  47. data/spec/lib/capistrano/doctor/variables_doctor_spec.rb +1 -1
  48. data/spec/lib/capistrano/dsl/task_enhancements_spec.rb +6 -6
  49. data/spec/lib/capistrano/dsl_spec.rb +5 -5
  50. data/spec/lib/capistrano/plugin_spec.rb +2 -2
  51. data/spec/lib/capistrano/scm/git_spec.rb +37 -5
  52. data/spec/spec_helper.rb +13 -0
  53. data/spec/support/test_app.rb +23 -14
  54. metadata +25 -73
  55. data/.travis.yml +0 -27
  56. data/Dangerfile +0 -1
  57. data/features/support/vagrant_helpers.rb +0 -35
  58. data/spec/support/.gitignore +0 -1
  59. 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 documention is stored in the `docs/` directory as Markdown files. These files are used to automatically generate the [capistranorb.com](http://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.
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. To hack on Capistrano you will further need some specialized tools to run its test suite.
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 Vagrant to deploy to a real virtual server.
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 Travis build will fail and your PR cannot be merged if Rubocop finds errors
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
- gem "cucumber"
8
- gem "rspec"
9
- gem "rspec-core", "~> 3.4.4"
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-2018 Tom Clements, Lee Hambley
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://travis-ci.org/capistrano/capistrano.svg?branch=master)](https://travis-ci.org/capistrano/capistrano) [![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)
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.11", require: false
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-2018 Tom Clements, Lee Hambley
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
- 4. Update the version in the `./README.md` Gemfile example (`gem "capistrano", "~> X.Y"`).
15
- 5. Update the `CHANGELOG`.
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
- task default: %i(spec rubocop)
7
- RSpec::Core::RakeTask.new
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
- desc "Run RuboCop checks"
12
- RuboCop::RakeTask.new
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 = "http://capistranorb.com/"
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
@@ -0,0 +1,8 @@
1
+ name: capistrano
2
+
3
+ services:
4
+ ssh_server:
5
+ build:
6
+ context: .docker
7
+ ports:
8
+ - "2022:22"
@@ -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
-
@@ -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+\(vagrant\)"
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.git_wrapper_path.shellescape}) == "700" ])
9
- _stdout, _stderr, status = vagrant_cli_command("ssh -c #{permissions_test.shellescape}")
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
- run_vagrant_command(test_dir_exists(TestApp.shared_path))
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
- run_vagrant_command(test_dir_exists(TestApp.releases_path))
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
- _, _, status = vagrant_cli_command("ssh -c #{test.shellescape}")
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
- _, _, status = vagrant_cli_command("ssh -c #{test.shellescape}")
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
- run_vagrant_command(test_dir_exists(TestApp.shared_path.join(dir)))
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
- run_vagrant_command(test_dir_exists(dir))
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
- run_vagrant_command(test_dir_exists(TestApp.repo_path))
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
- run_vagrant_command("ls -g #{TestApp.releases_path}")
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
- run_vagrant_command(test_symlink_exists(TestApp.current_path.join(file)))
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
- run_vagrant_command(test_symlink_exists(TestApp.release_path.join(dir)))
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
- run_vagrant_command(exists?("e", TestApp.current_path))
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
- run_vagrant_command(test_file_exists(TestApp.shared_path.join(file)))
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
- run_vagrant_command(test_file_exists(failed))
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 { run_vagrant_command(test_file_exists(failed)) }
122
- .to raise_error(VagrantHelpers::VagrantSSHCommandError)
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
- run_vagrant_command(test_file_exists(error))
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
- run_vagrant_command(symlinked?(TestApp.current_path, previous_release_path))
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
- run_vagrant_command(symlinked?(TestApp.current_path, specific_release_path))
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
- begin
11
- vagrant_cli_command("up")
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
- run_vagrant_command("mkdir -p #{file_shared_path.dirname}")
25
- run_vagrant_command("touch #{file_shared_path}")
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
- run_vagrant_command("mkdir -p #{TestApp.shared_path}")
37
- run_vagrant_command("touch #{file_shared_path} && rm #{file_shared_path}")
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$/) do |count|
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 = run_vagrant_command("readlink #{TestApp.current_path}")
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
- offset = -(a_day * num.to_i)
84
- num.to_i.times do
85
- run_vagrant_command("mkdir -p #{TestApp.release_path(TestApp.timestamp(offset))}")
86
- offset += a_day
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
- run_vagrant_command("mkdir -p #{TestApp.release_path(filename)}")
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
@@ -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
- run_vagrant_command("rm #{test_file}")
23
+ run_remote_ssh_command("rm #{test_file}")
24
24
  rescue
25
- VagrantHelpers::VagrantSSHCommandError
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 $stdin.tty?
39
+ return unless stdin.tty?
40
40
 
41
41
  if echo?
42
- $stdin.gets
42
+ stdin.gets
43
43
  else
44
- $stdin.noecho(&:gets).tap { $stdout.print "\n" }
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
@@ -68,7 +68,7 @@ module Capistrano
68
68
  # works as expected.
69
69
  #
70
70
  def assert_valid_later(key, callable)
71
- validation_callback = lambda do
71
+ validation_callback = proc do
72
72
  value = callable.call
73
73
  assert_valid_now(key, value)
74
74
  value
@@ -10,6 +10,8 @@ module Capistrano
10
10
  WHITELIST = %i(
11
11
  application
12
12
  current_directory
13
+ linked_dirs
14
+ linked_files
13
15
  releases_directory
14
16
  repo_url
15
17
  repo_tree
@@ -33,7 +33,7 @@ module Capistrano
33
33
  end
34
34
 
35
35
  def t(key, options={})
36
- I18n.t(key, options.merge(scope: :capistrano))
36
+ I18n.t(key, **options.merge(scope: :capistrano))
37
37
  end
38
38
 
39
39
  def scm